Compare commits
	
		
			No commits in common. "main" and "tentative-status" have entirely different histories.
		
	
	
		
			main
			...
			tentative-
		
	
		
| @ -1,58 +0,0 @@ | ||||
| --- | ||||
| :position: before | ||||
| :position_in_additional_file_patterns: before | ||||
| :position_in_class: before | ||||
| :position_in_factory: before | ||||
| :position_in_fixture: before | ||||
| :position_in_routes: before | ||||
| :position_in_serializer: before | ||||
| :position_in_test: before | ||||
| :classified_sort: true | ||||
| :exclude_controllers: true | ||||
| :exclude_factories: true | ||||
| :exclude_fixtures: false | ||||
| :exclude_helpers: true | ||||
| :exclude_scaffolds: true | ||||
| :exclude_serializers: false | ||||
| :exclude_sti_subclasses: false | ||||
| :exclude_tests: true | ||||
| :force: false | ||||
| :format_markdown: false | ||||
| :format_rdoc: false | ||||
| :format_yard: false | ||||
| :frozen: false | ||||
| :ignore_model_sub_dir: false | ||||
| :ignore_unknown_models: false | ||||
| :include_version: false | ||||
| :show_check_constraints: false | ||||
| :show_complete_foreign_keys: false | ||||
| :show_foreign_keys: true | ||||
| :show_indexes: true | ||||
| :simple_indexes: false | ||||
| :sort: false | ||||
| :timestamp: false | ||||
| :trace: false | ||||
| :with_comment: true | ||||
| :with_column_comments: true | ||||
| :with_table_comments: true | ||||
| :active_admin: false | ||||
| :command:  | ||||
| :debug: false | ||||
| :hide_default_column_types: '' | ||||
| :hide_limit_column_types: '' | ||||
| :ignore_columns:  | ||||
| :ignore_routes:  | ||||
| :models: true | ||||
| :routes: false | ||||
| :skip_on_db_migrate: false | ||||
| :target_action: :do_annotations | ||||
| :wrapper:  | ||||
| :wrapper_close:  | ||||
| :wrapper_open:  | ||||
| :classes_default_to_s: [] | ||||
| :additional_file_patterns: [] | ||||
| :model_dir: | ||||
| - app/models | ||||
| :require: [] | ||||
| :root_dir: | ||||
| - '' | ||||
| @ -35,4 +35,3 @@ | ||||
| /app/assets/builds/* | ||||
| !/app/assets/builds/.keep | ||||
| /public/assets | ||||
| .docker-compose.yml | ||||
| @ -1,159 +0,0 @@ | ||||
| name: Run unit tests | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
| concurrency: | ||||
|   group: ${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| jobs: | ||||
|   unit_tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     services: &services | ||||
|       postgres: | ||||
|         image: postgres | ||||
|         env: | ||||
|           POSTGRES_USER: postgres | ||||
|           POSTGRES_PASSWORD: postgres | ||||
|         ports: | ||||
|           - 5432 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|         with: | ||||
|           token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           ref: ${{ github.head_ref }} # Checkout the actual branch, not the result if merged into the base | ||||
|       - uses: ruby/setup-ruby@v1 | ||||
|       - run: bundle install | ||||
|       - &postgres_wait | ||||
|         name: Wait until Postgres is ready to accept connections | ||||
|         run: | | ||||
|           apt-get update && apt-get install -f -y postgresql-client | ||||
|           until pg_isready -h postgres -U postgres -d postgres | ||||
|           do | ||||
|             sleep 1 | ||||
|             echo "Trying again" | ||||
|           done | ||||
|       - name: Load schema and run unit tests | ||||
|         run: | | ||||
|           bundle exec rake db:schema:load | ||||
|           bundle exec rspec | ||||
|         env: | ||||
|           RAILS_ENV: test | ||||
|           DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres | ||||
|       - name: Get all migrations added | ||||
|         id: changed-migration-files | ||||
|         uses: tj-actions/changed-files@v45 | ||||
|         with: | ||||
|           files: | | ||||
|             db/migrate/**.rb | ||||
|       - name: Redo all migrations and check there are no schema changes | ||||
|         if: steps.changed-migration-files.outputs.any_changed == 'true' | ||||
|         env: | ||||
|           ALL_CHANGED_FILES: ${{ steps.changed-migration-files.outputs.all_changed_files }} | ||||
|           RAILS_ENV: test | ||||
|           DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres | ||||
|         run: | | ||||
|           echo ${#ALL_CHANGED_FILES[@]} migrations changed: | ||||
|           for file in ${ALL_CHANGED_FILES}; do | ||||
|             echo "$file" | ||||
|           done | ||||
| 
 | ||||
|           bundle exec rake db:migrate:redo STEP=${#ALL_CHANGED_FILES[@]} | ||||
|           git diff --exit-code db/schema.rb | ||||
|       - name: Clean up containers generated by this flow | ||||
|         if: failure() | ||||
|         run: docker ps --filter network=$JOB_CONTAINER_NAME-$GITHUB_JOB-network --filter name=$JOB_CONTAINER_NAME-* --format "{{.ID}}" | xargs docker rm -f | ||||
|   rubocop: | ||||
|     if: github.event_name == 'pull_request' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|         with: | ||||
|           token: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - uses: ruby/setup-ruby@v1 | ||||
|       - run: bundle install | ||||
|       - run: bundle exec rubocop --force-exclusion --parallel | ||||
|   check-licenses: | ||||
|     if: github.event_name == 'pull_request' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|         - uses: actions/checkout@v5 | ||||
|           with: | ||||
|             token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         - uses: ruby/setup-ruby@v1 | ||||
|         - name: Install project dependencies | ||||
|           run: bundle install --jobs `getconf _NPROCESSORS_ONLN` | ||||
|         - name: Run license finder | ||||
|           run: license_finder | ||||
|   copyright_notice: | ||||
|     if: github.event_name == 'pull_request' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|         with: | ||||
|           token: ${{ secrets.ACTIONS_TOKEN }} | ||||
|           ref: ${{ github.head_ref }} | ||||
|       - uses: VinnyBabuManjaly/copyright-action@v1.0.0 | ||||
|         with: | ||||
|           CopyrightString: '# Copyright (C) 2024-2025 LibreWeddingPlanner contributors\n\n' | ||||
|           FileType: '.rb' | ||||
|           Path: 'app/, config/, db/, spec/' | ||||
|           IgnorePath: 'db' | ||||
|       - uses: VinnyBabuManjaly/copyright-action@v1.0.0 | ||||
|         with: | ||||
|           CopyrightString: '<%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %>\n\n' | ||||
|           FileType: '.erb' | ||||
|           Path: 'app/' | ||||
|       - name: Commit changes | ||||
|         run: | | ||||
|           git config --local user.email "bustikiller@bustikiller.com" | ||||
|           git config --local user.name "Manuel Bustillo" | ||||
|           git add . | ||||
| 
 | ||||
|           if [ -n "$(git status --porcelain)" ]; then | ||||
|             echo "there are changes"; | ||||
|             git commit -m "Add copyright notice" | ||||
|             git push | ||||
|           else | ||||
|             echo "no changes"; | ||||
|           fi | ||||
| 
 | ||||
|   build-static-assets: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 30 | ||||
|     needs: | ||||
|       - unit_tests | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|         with: | ||||
|           token: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|        | ||||
|       - name: Login to the private Docker registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ${{ secrets.PRIVATE_REGISTRY_HOST }} | ||||
|           username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }} | ||||
|           password: ${{ secrets.PRIVATE_REGISTRY_TOKEN }} | ||||
| 
 | ||||
|       - name: Build and push intermediate stages (build) | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           target: build | ||||
|           push: ${{ github.ref == 'refs/heads/main' }} | ||||
|           tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build | ||||
|           cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build | ||||
|           cache-to: type=inline | ||||
| 
 | ||||
|       - name: Build and push (final) | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           push: ${{ github.ref == 'refs/heads/main' }} | ||||
|           tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest | ||||
|           cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest | ||||
|           cache-to: type=inline | ||||
							
								
								
									
										33
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| name: Build Nginx-based docker image | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
| jobs: | ||||
|   build-static-assets: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|           token: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|        | ||||
|       - name: Login to the private Docker registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ${{ secrets.PRIVATE_REGISTRY_HOST }} | ||||
|           username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }} | ||||
|           password: ${{ secrets.PRIVATE_REGISTRY_TOKEN }} | ||||
|          | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           tags: | | ||||
|             ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest | ||||
|           cache-from: type=registry,ref=user/app:latest | ||||
|           cache-to: type=inline | ||||
							
								
								
									
										40
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| name: Run unit tests | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
| jobs: | ||||
|   unit_tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     services: | ||||
|       postgres: | ||||
|         image: postgres | ||||
|         env: | ||||
|           POSTGRES_USER: postgres | ||||
|           POSTGRES_PASSWORD: postgres | ||||
|         ports: | ||||
|           - 5432 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|           token: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - uses: ruby/setup-ruby@v1 | ||||
|       - run: bundle install | ||||
|       - name: Wait until Postgres is ready to accept connections | ||||
|         run: | | ||||
|           apt-get update && apt-get install -f -y postgresql-client | ||||
|           until pg_isready -h postgres -U postgres -d postgres | ||||
|           do | ||||
|             sleep 1 | ||||
|             echo "Trying again" | ||||
|           done | ||||
|       - run: | | ||||
|           bundle exec rake db:schema:load | ||||
|           bundle exec rspec | ||||
|         env: | ||||
|           RAILS_ENV: test | ||||
|           DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres | ||||
|       - name: Clean up containers generated by this flow | ||||
|         if: failure() | ||||
|         run: docker ps --filter network=$JOB_CONTAINER_NAME-$GITHUB_JOB-network --filter name=$JOB_CONTAINER_NAME-* --format "{{.ID}}" | xargs docker rm -f | ||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -33,7 +33,3 @@ | ||||
| 
 | ||||
| # Ignore master key for decrypting credentials and more. | ||||
| /config/master.key | ||||
| 
 | ||||
| # Ignore swagger generated documentation | ||||
| swagger/v1/swagger.yaml | ||||
| wedding-planner.code-workspace | ||||
|  | ||||
							
								
								
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.rubocop.yml
									
									
									
									
									
								
							| @ -1,29 +0,0 @@ | ||||
| require: | ||||
|   - rubocop-rails | ||||
|   - rubocop-factory_bot | ||||
|   - rubocop-rspec | ||||
|   - rubocop-rspec_rails | ||||
| AllCops: | ||||
|   NewCops: enable | ||||
|   Exclude: | ||||
|     - 'db/**/*' | ||||
|     - 'config/**/*' | ||||
|     - 'script/**/*' | ||||
|     - 'bin/*' | ||||
|     - '*.yml' | ||||
| Layout/LineLength: | ||||
|   Max: 120 | ||||
| RSpec/ExampleLength: | ||||
|   Enabled: false | ||||
| Metrics/ModuleLength: | ||||
|   Enabled: false | ||||
| RSpec/MultipleMemoizedHelpers: | ||||
|   Enabled: false | ||||
| Style/Documentation: | ||||
|   Enabled: false | ||||
| Metrics/MethodLength: | ||||
|   Max: 20 | ||||
| Rails/SkipsModelValidations: | ||||
|   Enabled: false | ||||
| Metrics/AbcSize: | ||||
|   Enabled: false | ||||
| @ -1 +1 @@ | ||||
| ruby-3.4.3 | ||||
| ruby-3.3.5 | ||||
|  | ||||
							
								
								
									
										660
									
								
								COPYING.md
									
									
									
									
									
								
							
							
						
						
									
										660
									
								
								COPYING.md
									
									
									
									
									
								
							| @ -1,660 +0,0 @@ | ||||
| # GNU AFFERO GENERAL PUBLIC LICENSE | ||||
| 
 | ||||
| Version 3, 19 November 2007 | ||||
| 
 | ||||
| Copyright (C) 2007 Free Software Foundation, Inc. | ||||
| <https://fsf.org/> | ||||
| 
 | ||||
| Everyone is permitted to copy and distribute verbatim copies of this | ||||
| license document, but changing it is not allowed. | ||||
| 
 | ||||
| ## Preamble | ||||
| 
 | ||||
| The GNU Affero General Public License is a free, copyleft license for | ||||
| software and other kinds of works, specifically designed to ensure | ||||
| cooperation with the community in the case of network server software. | ||||
| 
 | ||||
| The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works. By contrast, | ||||
| our General Public Licenses are intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains | ||||
| free software for all its users. | ||||
| 
 | ||||
| When we speak of free software, we are referring to freedom, not | ||||
| price. Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
| 
 | ||||
| Developers that use our General Public Licenses protect your rights | ||||
| with two steps: (1) assert copyright on the software, and (2) offer | ||||
| you this License which gives you legal permission to copy, distribute | ||||
| and/or modify the software. | ||||
| 
 | ||||
| A secondary benefit of defending all users' freedom is that | ||||
| improvements made in alternate versions of the program, if they | ||||
| receive widespread use, become available for other developers to | ||||
| incorporate. Many developers of free software are heartened and | ||||
| encouraged by the resulting cooperation. However, in the case of | ||||
| software used on network servers, this result may fail to come about. | ||||
| The GNU General Public License permits making a modified version and | ||||
| letting the public access it on a server without ever releasing its | ||||
| source code to the public. | ||||
| 
 | ||||
| The GNU Affero General Public License is designed specifically to | ||||
| ensure that, in such cases, the modified source code becomes available | ||||
| to the community. It requires the operator of a network server to | ||||
| provide the source code of the modified version running there to the | ||||
| users of that server. Therefore, public use of a modified version, on | ||||
| a publicly accessible server, gives the public access to the source | ||||
| code of the modified version. | ||||
| 
 | ||||
| An older license, called the Affero General Public License and | ||||
| published by Affero, was designed to accomplish similar goals. This is | ||||
| a different license, not a version of the Affero GPL, but Affero has | ||||
| released a new version of the Affero GPL which permits relicensing | ||||
| under this license. | ||||
| 
 | ||||
| The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
| 
 | ||||
| ## TERMS AND CONDITIONS | ||||
| 
 | ||||
| ### 0. Definitions. | ||||
| 
 | ||||
| "This License" refers to version 3 of the GNU Affero General Public | ||||
| License. | ||||
| 
 | ||||
| "Copyright" also means copyright-like laws that apply to other kinds | ||||
| of works, such as semiconductor masks. | ||||
| 
 | ||||
| "The Program" refers to any copyrightable work licensed under this | ||||
| License. Each licensee is addressed as "you". "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
| 
 | ||||
| To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of | ||||
| an exact copy. The resulting work is called a "modified version" of | ||||
| the earlier work or a work "based on" the earlier work. | ||||
| 
 | ||||
| A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
| 
 | ||||
| To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy. Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
| 
 | ||||
| To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies. Mere interaction with a user | ||||
| through a computer network, with no transfer of a copy, is not | ||||
| conveying. | ||||
| 
 | ||||
| An interactive user interface displays "Appropriate Legal Notices" to | ||||
| the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License. If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
| 
 | ||||
| ### 1. Source Code. | ||||
| 
 | ||||
| The "source code" for a work means the preferred form of the work for | ||||
| making modifications to it. "Object code" means any non-source form of | ||||
| a work. | ||||
| 
 | ||||
| A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
| 
 | ||||
| The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form. A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
| 
 | ||||
| The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities. However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work. For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
| 
 | ||||
| The Corresponding Source need not include anything that users can | ||||
| regenerate automatically from other parts of the Corresponding Source. | ||||
| 
 | ||||
| The Corresponding Source for a work in source code form is that same | ||||
| work. | ||||
| 
 | ||||
| ### 2. Basic Permissions. | ||||
| 
 | ||||
| All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met. This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program. The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work. This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
| 
 | ||||
| You may make, run and propagate covered works that you do not convey, | ||||
| without conditions so long as your license otherwise remains in force. | ||||
| You may convey covered works to others for the sole purpose of having | ||||
| them make modifications exclusively for you, or provide you with | ||||
| facilities for running those works, provided that you comply with the | ||||
| terms of this License in conveying all material for which you do not | ||||
| control copyright. Those thus making or running the covered works for | ||||
| you must do so exclusively on your behalf, under your direction and | ||||
| control, on terms that prohibit them from making any copies of your | ||||
| copyrighted material outside their relationship with you. | ||||
| 
 | ||||
| Conveying under any other circumstances is permitted solely under the | ||||
| conditions stated below. Sublicensing is not allowed; section 10 makes | ||||
| it unnecessary. | ||||
| 
 | ||||
| ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
| 
 | ||||
| No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
| 
 | ||||
| When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such | ||||
| circumvention is effected by exercising rights under this License with | ||||
| respect to the covered work, and you disclaim any intention to limit | ||||
| operation or modification of the work as a means of enforcing, against | ||||
| the work's users, your or third parties' legal rights to forbid | ||||
| circumvention of technological measures. | ||||
| 
 | ||||
| ### 4. Conveying Verbatim Copies. | ||||
| 
 | ||||
| You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
| 
 | ||||
| You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
| 
 | ||||
| ### 5. Conveying Modified Source Versions. | ||||
| 
 | ||||
| You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these | ||||
| conditions: | ||||
| 
 | ||||
| -   a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
| -   b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under | ||||
|     section 7. This requirement modifies the requirement in section 4 | ||||
|     to "keep intact all notices". | ||||
| -   c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy. This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged. This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
| -   d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
| 
 | ||||
| A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit. Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
| 
 | ||||
| ### 6. Conveying Non-Source Forms. | ||||
| 
 | ||||
| You may convey a covered work in object code form under the terms of | ||||
| sections 4 and 5, provided that you also convey the machine-readable | ||||
| Corresponding Source under the terms of this License, in one of these | ||||
| ways: | ||||
| 
 | ||||
| -   a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
| -   b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the Corresponding | ||||
|     Source from a network server at no charge. | ||||
| -   c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source. This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
| -   d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge. You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code. If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source. Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
| -   e) Convey the object code using peer-to-peer transmission, | ||||
|     provided you inform other peers where the object code and | ||||
|     Corresponding Source of the work are being offered to the general | ||||
|     public at no charge under subsection 6d. | ||||
| 
 | ||||
| A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
| 
 | ||||
| A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, | ||||
| family, or household purposes, or (2) anything designed or sold for | ||||
| incorporation into a dwelling. In determining whether a product is a | ||||
| consumer product, doubtful cases shall be resolved in favor of | ||||
| coverage. For a particular product received by a particular user, | ||||
| "normally used" refers to a typical or common use of that class of | ||||
| product, regardless of the status of the particular user or of the way | ||||
| in which the particular user actually uses, or expects or is expected | ||||
| to use, the product. A product is a consumer product regardless of | ||||
| whether the product has substantial commercial, industrial or | ||||
| non-consumer uses, unless such uses represent the only significant | ||||
| mode of use of the product. | ||||
| 
 | ||||
| "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to | ||||
| install and execute modified versions of a covered work in that User | ||||
| Product from a modified version of its Corresponding Source. The | ||||
| information must suffice to ensure that the continued functioning of | ||||
| the modified object code is in no case prevented or interfered with | ||||
| solely because modification has been made. | ||||
| 
 | ||||
| If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information. But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
| 
 | ||||
| The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or | ||||
| updates for a work that has been modified or installed by the | ||||
| recipient, or for the User Product in which it has been modified or | ||||
| installed. Access to a network may be denied when the modification | ||||
| itself materially and adversely affects the operation of the network | ||||
| or violates the rules and protocols for communication across the | ||||
| network. | ||||
| 
 | ||||
| Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
| 
 | ||||
| ### 7. Additional Terms. | ||||
| 
 | ||||
| "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law. If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
| 
 | ||||
| When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it. (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.) You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
| 
 | ||||
| Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders | ||||
| of that material) supplement the terms of this License with terms: | ||||
| 
 | ||||
| -   a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
| -   b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
| -   c) Prohibiting misrepresentation of the origin of that material, | ||||
|     or requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
| -   d) Limiting the use for publicity purposes of names of licensors | ||||
|     or authors of the material; or | ||||
| -   e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
| -   f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions | ||||
|     of it) with contractual assumptions of liability to the recipient, | ||||
|     for any liability that these contractual assumptions directly | ||||
|     impose on those licensors and authors. | ||||
| 
 | ||||
| All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10. If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term. If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
| 
 | ||||
| If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
| 
 | ||||
| Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; the | ||||
| above requirements apply either way. | ||||
| 
 | ||||
| ### 8. Termination. | ||||
| 
 | ||||
| You may not propagate or modify a covered work except as expressly | ||||
| provided under this License. Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
| 
 | ||||
| However, if you cease all violation of this License, then your license | ||||
| from a particular copyright holder is reinstated (a) provisionally, | ||||
| unless and until the copyright holder explicitly and finally | ||||
| terminates your license, and (b) permanently, if the copyright holder | ||||
| fails to notify you of the violation by some reasonable means prior to | ||||
| 60 days after the cessation. | ||||
| 
 | ||||
| Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
| 
 | ||||
| Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License. If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
| 
 | ||||
| ### 9. Acceptance Not Required for Having Copies. | ||||
| 
 | ||||
| You are not required to accept this License in order to receive or run | ||||
| a copy of the Program. Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance. However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work. These actions infringe copyright if you do | ||||
| not accept this License. Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
| 
 | ||||
| ### 10. Automatic Licensing of Downstream Recipients. | ||||
| 
 | ||||
| Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License. You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
| 
 | ||||
| An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations. If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
| 
 | ||||
| You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License. For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
| 
 | ||||
| ### 11. Patents. | ||||
| 
 | ||||
| A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based. The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
| 
 | ||||
| A contributor's "essential patent claims" are all patent claims owned | ||||
| or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version. For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
| 
 | ||||
| Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
| 
 | ||||
| In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement). To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
| 
 | ||||
| If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients. "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
| 
 | ||||
| If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
| 
 | ||||
| A patent license is "discriminatory" if it does not include within the | ||||
| scope of its coverage, prohibits the exercise of, or is conditioned on | ||||
| the non-exercise of one or more of the rights that are specifically | ||||
| granted under this License. You may not convey a covered work if you | ||||
| are a party to an arrangement with a third party that is in the | ||||
| business of distributing software, under which you make payment to the | ||||
| third party based on the extent of your activity of conveying the | ||||
| work, and under which the third party grants, to any of the parties | ||||
| who would receive the covered work from you, a discriminatory patent | ||||
| license (a) in connection with copies of the covered work conveyed by | ||||
| you (or copies made from those copies), or (b) primarily for and in | ||||
| connection with specific products or compilations that contain the | ||||
| covered work, unless you entered into that arrangement, or that patent | ||||
| license was granted, prior to 28 March 2007. | ||||
| 
 | ||||
| Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
| 
 | ||||
| ### 12. No Surrender of Others' Freedom. | ||||
| 
 | ||||
| If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License. If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under | ||||
| this License and any other pertinent obligations, then as a | ||||
| consequence you may not convey it at all. For example, if you agree to | ||||
| terms that obligate you to collect a royalty for further conveying | ||||
| from those to whom you convey the Program, the only way you could | ||||
| satisfy both those terms and this License would be to refrain entirely | ||||
| from conveying the Program. | ||||
| 
 | ||||
| ### 13. Remote Network Interaction; Use with the GNU General Public License. | ||||
| 
 | ||||
| Notwithstanding any other provision of this License, if you modify the | ||||
| Program, your modified version must prominently offer all users | ||||
| interacting with it remotely through a computer network (if your | ||||
| version supports such interaction) an opportunity to receive the | ||||
| Corresponding Source of your version by providing access to the | ||||
| Corresponding Source from a network server at no charge, through some | ||||
| standard or customary means of facilitating copying of software. This | ||||
| Corresponding Source shall include the Corresponding Source for any | ||||
| work covered by version 3 of the GNU General Public License that is | ||||
| incorporated pursuant to the following paragraph. | ||||
| 
 | ||||
| Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU General Public License into a single | ||||
| combined work, and to convey the resulting work. The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the work with which it is combined will remain governed by version | ||||
| 3 of the GNU General Public License. | ||||
| 
 | ||||
| ### 14. Revised Versions of this License. | ||||
| 
 | ||||
| The Free Software Foundation may publish revised and/or new versions | ||||
| of the GNU Affero General Public License from time to time. Such new | ||||
| versions will be similar in spirit to the present version, but may | ||||
| differ in detail to address new problems or concerns. | ||||
| 
 | ||||
| Each version is given a distinguishing version number. If the Program | ||||
| specifies that a certain numbered version of the GNU Affero General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation. If the Program does not specify a version number of the | ||||
| GNU Affero General Public License, you may choose any version ever | ||||
| published by the Free Software Foundation. | ||||
| 
 | ||||
| If the Program specifies that a proxy can decide which future versions | ||||
| of the GNU Affero General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
| 
 | ||||
| Later license versions may give you additional or different | ||||
| permissions. However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
| 
 | ||||
| ### 15. Disclaimer of Warranty. | ||||
| 
 | ||||
| THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT | ||||
| WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND | ||||
| PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE | ||||
| DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR | ||||
| CORRECTION. | ||||
| 
 | ||||
| ### 16. Limitation of Liability. | ||||
| 
 | ||||
| IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR | ||||
| CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, | ||||
| INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES | ||||
| ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT | ||||
| NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR | ||||
| LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM | ||||
| TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER | ||||
| PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. | ||||
| 
 | ||||
| ### 17. Interpretation of Sections 15 and 16. | ||||
| 
 | ||||
| If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
| 
 | ||||
| END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
| ## How to Apply These Terms to Your New Programs | ||||
| 
 | ||||
| If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these | ||||
| terms. | ||||
| 
 | ||||
| To do so, attach the following notices to the program. It is safest to | ||||
| attach them to the start of each source file to most effectively state | ||||
| the exclusion of warranty; and each file should have at least the | ||||
| "copyright" line and a pointer to where the full notice is found. | ||||
| 
 | ||||
|         <one line to give the program's name and a brief idea of what it does.> | ||||
|         Copyright (C) <year>  <name of author> | ||||
| 
 | ||||
|         This program is free software: you can redistribute it and/or modify | ||||
|         it under the terms of the GNU Affero General Public License as | ||||
|         published by the Free Software Foundation, either version 3 of the | ||||
|         License, or (at your option) any later version. | ||||
| 
 | ||||
|         This program is distributed in the hope that it will be useful, | ||||
|         but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|         GNU Affero General Public License for more details. | ||||
| 
 | ||||
|         You should have received a copy of the GNU Affero General Public License | ||||
|         along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| Also add information on how to contact you by electronic and paper | ||||
| mail. | ||||
| 
 | ||||
| If your software can interact with users remotely through a computer | ||||
| network, you should also make sure that it provides a way for users to | ||||
| get its source. For example, if your program is a web application, its | ||||
| interface could display a "Source" link that leads users to an archive | ||||
| of the code. There are many ways you could offer source, and different | ||||
| solutions will be better for different programs; see section 13 for | ||||
| the specific requirements. | ||||
| 
 | ||||
| You should also get your employer (if you work as a programmer) or | ||||
| school, if any, to sign a "copyright disclaimer" for the program, if | ||||
| necessary. For more information on this, and how to apply and follow | ||||
| the GNU AGPL, see <https://www.gnu.org/licenses/>. | ||||
							
								
								
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | ||||
| # syntax = docker/dockerfile:1 | ||||
| 
 | ||||
| # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile | ||||
| ARG RUBY_VERSION=3.4.3 | ||||
| FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base | ||||
| ARG RUBY_VERSION=3.3.5 | ||||
| FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base | ||||
| 
 | ||||
| # Rails app lives here | ||||
| WORKDIR /rails | ||||
| @ -13,14 +13,14 @@ ENV RAILS_ENV="production" \ | ||||
|     BUNDLE_PATH="/usr/local/bundle" \ | ||||
|     BUNDLE_WITHOUT="development" | ||||
| 
 | ||||
| RUN apt-get update && apt-get install -y nodejs wkhtmltopdf | ||||
| RUN apt-get update && apt-get install -y nodejs | ||||
| 
 | ||||
| # Throw-away build stage to reduce size of final image | ||||
| FROM base AS build | ||||
| FROM base as build | ||||
| 
 | ||||
| # Install packages needed to build gems | ||||
| RUN apt-get update -qq && \ | ||||
|     apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config libyaml-dev | ||||
|     apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config | ||||
| 
 | ||||
| # Install application gems | ||||
| COPY Gemfile Gemfile.lock ./ | ||||
|  | ||||
| @ -1,42 +0,0 @@ | ||||
| # syntax = docker/dockerfile:1 | ||||
| 
 | ||||
| # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile | ||||
| ARG RUBY_VERSION=3.4.3 | ||||
| FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base | ||||
| 
 | ||||
| # Rails app lives here | ||||
| WORKDIR /rails | ||||
| 
 | ||||
| RUN apt-get update && apt-get install -y nodejs wkhtmltopdf | ||||
| 
 | ||||
| FROM base as build | ||||
| 
 | ||||
| # Install packages needed to build gems | ||||
| RUN apt-get update -qq && \ | ||||
|     apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config libyaml-dev | ||||
| 
 | ||||
| # Install application gems | ||||
| COPY Gemfile Gemfile.lock ./ | ||||
| RUN bundle install | ||||
| 
 | ||||
| # Copy application code | ||||
| COPY . . | ||||
| 
 | ||||
| # Final stage for app image | ||||
| FROM base | ||||
| 
 | ||||
| # Install packages needed for deployment | ||||
| RUN apt-get update -qq && \ | ||||
|     apt-get install --no-install-recommends -y curl libvips postgresql-client && \ | ||||
|     rm -rf /var/lib/apt/lists /var/cache/apt/archives | ||||
| 
 | ||||
| # Copy built artifacts: gems, application | ||||
| COPY --from=build /usr/local/bundle /usr/local/bundle | ||||
| COPY --from=build /rails /rails | ||||
| 
 | ||||
| # Entrypoint prepares the database. | ||||
| ENTRYPOINT ["/rails/bin/docker-entrypoint"] | ||||
| 
 | ||||
| # Start the server by default, this can be overwritten at runtime | ||||
| EXPOSE 3000 | ||||
| CMD ["./bin/rails", "server", "--binding=0.0.0.0"] | ||||
							
								
								
									
										32
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								Gemfile
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| source 'https://rubygems.org' | ||||
| 
 | ||||
| ruby '3.4.3' | ||||
| ruby '3.3.5' | ||||
| gem 'acts-as-taggable-on' | ||||
| gem 'bootsnap', require: false | ||||
| gem 'csv' | ||||
| gem 'importmap-rails' | ||||
| @ -10,48 +9,27 @@ gem 'jbuilder' | ||||
| gem 'money' | ||||
| gem 'pg', '~> 1.1' | ||||
| gem 'puma', '>= 5.0' | ||||
| gem 'rails', '~> 8.0.0', '>= 8.0.0' | ||||
| gem 'rails', '~> 7.2.0', '>= 7.2.1' | ||||
| gem 'redis', '>= 4.0.1' | ||||
| gem 'sprockets-rails' | ||||
| gem 'stimulus-rails' | ||||
| gem 'turbo-rails' | ||||
| gem 'tzinfo-data', platforms: %i[windows jruby] | ||||
| 
 | ||||
| gem 'acts_as_tenant' | ||||
| gem 'faker' | ||||
| gem 'httparty' | ||||
| gem 'jsonapi-rails' | ||||
| gem 'pluck_to_hash' | ||||
| gem 'rack-cors' | ||||
| gem 'react-rails' | ||||
| gem 'rswag' | ||||
| gem 'rubytree' | ||||
| 
 | ||||
| group :development, :test do | ||||
|   gem 'annotaterb' | ||||
|   gem 'debug', platforms: %i[mri windows] | ||||
|   gem 'factory_bot_rails' | ||||
|   gem 'license_finder' | ||||
|   gem 'faker' | ||||
|   gem 'pry' | ||||
|   gem 'rspec-rails', '~> 8.0.0' | ||||
|   gem 'shoulda-matchers', '~> 6.0' | ||||
|   gem 'rspec-rails', '~> 7.0.0' | ||||
| end | ||||
| 
 | ||||
| group :development do | ||||
|   gem 'letter_opener_web' | ||||
|   gem 'rubocop' | ||||
|   gem 'rubocop-factory_bot', require: false | ||||
|   gem 'rubocop-rails', require: false | ||||
|   gem 'rubocop-rspec', require: false | ||||
|   gem 'rubocop-rspec_rails', require: false | ||||
|   gem 'web-console' | ||||
| end | ||||
| 
 | ||||
| gem 'chroma' | ||||
| gem 'solid_queue', '~> 1.0' | ||||
| 
 | ||||
| gem 'devise', '~> 4.9' | ||||
| 
 | ||||
| gem 'wicked_pdf', '~> 2.8' | ||||
| 
 | ||||
| gem 'rqrcode', '~> 3.1' | ||||
|  | ||||
							
								
								
									
										622
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										622
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -1,67 +1,67 @@ | ||||
| GEM | ||||
|   remote: https://rubygems.org/ | ||||
|   specs: | ||||
|     actioncable (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actioncable (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       nio4r (~> 2.0) | ||||
|       websocket-driver (>= 0.6.1) | ||||
|       zeitwerk (~> 2.6) | ||||
|     actionmailbox (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       activejob (= 8.0.2.1) | ||||
|       activerecord (= 8.0.2.1) | ||||
|       activestorage (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actionmailbox (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       activejob (= 7.2.1.2) | ||||
|       activerecord (= 7.2.1.2) | ||||
|       activestorage (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       mail (>= 2.8.0) | ||||
|     actionmailer (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       actionview (= 8.0.2.1) | ||||
|       activejob (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actionmailer (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       actionview (= 7.2.1.2) | ||||
|       activejob (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       mail (>= 2.8.0) | ||||
|       rails-dom-testing (~> 2.2) | ||||
|     actionpack (8.0.2.1) | ||||
|       actionview (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actionpack (7.2.1.2) | ||||
|       actionview (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       nokogiri (>= 1.8.5) | ||||
|       rack (>= 2.2.4) | ||||
|       racc | ||||
|       rack (>= 2.2.4, < 3.2) | ||||
|       rack-session (>= 1.0.1) | ||||
|       rack-test (>= 0.6.3) | ||||
|       rails-dom-testing (~> 2.2) | ||||
|       rails-html-sanitizer (~> 1.6) | ||||
|       useragent (~> 0.16) | ||||
|     actiontext (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       activerecord (= 8.0.2.1) | ||||
|       activestorage (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actiontext (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       activerecord (= 7.2.1.2) | ||||
|       activestorage (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       globalid (>= 0.6.0) | ||||
|       nokogiri (>= 1.8.5) | ||||
|     actionview (8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     actionview (7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       builder (~> 3.1) | ||||
|       erubi (~> 1.11) | ||||
|       rails-dom-testing (~> 2.2) | ||||
|       rails-html-sanitizer (~> 1.6) | ||||
|     activejob (8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     activejob (7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       globalid (>= 0.3.6) | ||||
|     activemodel (8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     activerecord (8.0.2.1) | ||||
|       activemodel (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     activemodel (7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|     activerecord (7.2.1.2) | ||||
|       activemodel (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       timeout (>= 0.4.0) | ||||
|     activestorage (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       activejob (= 8.0.2.1) | ||||
|       activerecord (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|     activestorage (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       activejob (= 7.2.1.2) | ||||
|       activerecord (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       marcel (~> 1.0) | ||||
|     activesupport (8.0.2.1) | ||||
|     activesupport (7.2.1.2) | ||||
|       base64 | ||||
|       benchmark (>= 0.3) | ||||
|       bigdecimal | ||||
|       concurrent-ruby (~> 1.0, >= 1.3.1) | ||||
|       connection_pool (>= 2.2.5) | ||||
| @ -71,86 +71,56 @@ GEM | ||||
|       minitest (>= 5.1) | ||||
|       securerandom (>= 0.3) | ||||
|       tzinfo (~> 2.0, >= 2.0.5) | ||||
|       uri (>= 0.13.1) | ||||
|     acts_as_tenant (1.0.1) | ||||
|       rails (>= 6.0) | ||||
|     addressable (2.8.7) | ||||
|       public_suffix (>= 2.0.2, < 7.0) | ||||
|     annotaterb (4.19.0) | ||||
|       activerecord (>= 6.0.0) | ||||
|       activesupport (>= 6.0.0) | ||||
|     ast (2.4.3) | ||||
|     acts-as-taggable-on (11.0.0) | ||||
|       activerecord (>= 7.0, < 8.0) | ||||
|       zeitwerk (>= 2.4, < 3.0) | ||||
|     ast (2.4.2) | ||||
|     babel-source (5.8.35) | ||||
|     babel-transpiler (0.7.0) | ||||
|       babel-source (>= 4.0, < 6) | ||||
|       execjs (~> 2.0) | ||||
|     base64 (0.3.0) | ||||
|     bcrypt (3.1.20) | ||||
|     benchmark (0.4.1) | ||||
|     bigdecimal (3.2.3) | ||||
|     base64 (0.2.0) | ||||
|     bigdecimal (3.1.8) | ||||
|     bindex (0.8.1) | ||||
|     bootsnap (1.18.6) | ||||
|     bootsnap (1.18.4) | ||||
|       msgpack (~> 1.2) | ||||
|     builder (3.3.0) | ||||
|     childprocess (5.1.0) | ||||
|       logger (~> 1.5) | ||||
|     chroma (0.2.0) | ||||
|     chunky_png (1.4.0) | ||||
|     coderay (1.1.3) | ||||
|     concurrent-ruby (1.3.5) | ||||
|     connection_pool (2.5.4) | ||||
|     concurrent-ruby (1.3.4) | ||||
|     connection_pool (2.4.1) | ||||
|     crass (1.0.6) | ||||
|     csv (3.3.5) | ||||
|     date (3.4.1) | ||||
|     debug (1.11.0) | ||||
|     csv (3.3.0) | ||||
|     date (3.3.4) | ||||
|     debug (1.9.2) | ||||
|       irb (~> 1.10) | ||||
|       reline (>= 0.3.8) | ||||
|     devise (4.9.4) | ||||
|       bcrypt (~> 3.0) | ||||
|       orm_adapter (~> 0.1) | ||||
|       railties (>= 4.1.0) | ||||
|       responders | ||||
|       warden (~> 1.2.3) | ||||
|     diff-lcs (1.6.2) | ||||
|     drb (2.2.3) | ||||
|     erb (5.0.2) | ||||
|     erubi (1.13.1) | ||||
|     et-orbi (1.2.11) | ||||
|       tzinfo | ||||
|     diff-lcs (1.5.1) | ||||
|     drb (2.2.1) | ||||
|     erubi (1.13.0) | ||||
|     execjs (2.9.1) | ||||
|     factory_bot (6.5.5) | ||||
|       activesupport (>= 6.1.0) | ||||
|     factory_bot_rails (6.5.1) | ||||
|       factory_bot (~> 6.5) | ||||
|       railties (>= 6.1.0) | ||||
|     faker (3.5.2) | ||||
|     factory_bot (6.4.6) | ||||
|       activesupport (>= 5.0.0) | ||||
|     factory_bot_rails (6.4.3) | ||||
|       factory_bot (~> 6.4) | ||||
|       railties (>= 5.0.0) | ||||
|     faker (3.5.1) | ||||
|       i18n (>= 1.8.11, < 2) | ||||
|     fugit (1.11.1) | ||||
|       et-orbi (~> 1, >= 1.2.11) | ||||
|       raabro (~> 1.4) | ||||
|     globalid (1.2.1) | ||||
|       activesupport (>= 6.1) | ||||
|     httparty (0.23.1) | ||||
|       csv | ||||
|       mini_mime (>= 1.0.0) | ||||
|       multi_xml (>= 0.5.2) | ||||
|     i18n (1.14.7) | ||||
|     i18n (1.14.6) | ||||
|       concurrent-ruby (~> 1.0) | ||||
|     importmap-rails (2.2.2) | ||||
|     importmap-rails (2.0.3) | ||||
|       actionpack (>= 6.0.0) | ||||
|       activesupport (>= 6.0.0) | ||||
|       railties (>= 6.0.0) | ||||
|     io-console (0.8.1) | ||||
|     irb (1.15.2) | ||||
|       pp (>= 0.6.0) | ||||
|     io-console (0.7.2) | ||||
|     irb (1.14.1) | ||||
|       rdoc (>= 4.0.0) | ||||
|       reline (>= 0.4.2) | ||||
|     jbuilder (2.13.0) | ||||
|       actionview (>= 5.0.0) | ||||
|       activesupport (>= 5.0.0) | ||||
|     json (2.13.2) | ||||
|     json-schema (5.0.1) | ||||
|       addressable (~> 2.8) | ||||
|     json (2.7.2) | ||||
|     jsonapi-deserializable (0.2.0) | ||||
|     jsonapi-parser (0.1.1) | ||||
|     jsonapi-rails (0.4.1) | ||||
| @ -162,28 +132,9 @@ GEM | ||||
|     jsonapi-renderer (0.2.2) | ||||
|     jsonapi-serializable (0.3.1) | ||||
|       jsonapi-renderer (~> 0.2.0) | ||||
|     language_server-protocol (3.17.0.5) | ||||
|     launchy (3.0.1) | ||||
|       addressable (~> 2.8) | ||||
|       childprocess (~> 5.0) | ||||
|     letter_opener (1.10.0) | ||||
|       launchy (>= 2.2, < 4) | ||||
|     letter_opener_web (3.0.0) | ||||
|       actionmailer (>= 6.1) | ||||
|       letter_opener (~> 1.9) | ||||
|       railties (>= 6.1) | ||||
|       rexml | ||||
|     license_finder (7.2.1) | ||||
|       bundler | ||||
|       csv (~> 3.2) | ||||
|       rubyzip (>= 1, < 3) | ||||
|       thor (~> 1.2) | ||||
|       tomlrb (>= 1.3, < 2.1) | ||||
|       with_env (= 1.1.0) | ||||
|       xml-simple (~> 1.1.9) | ||||
|     lint_roller (1.1.0) | ||||
|     logger (1.7.0) | ||||
|     loofah (2.24.1) | ||||
|     language_server-protocol (3.17.0.3) | ||||
|     logger (1.6.1) | ||||
|     loofah (2.22.0) | ||||
|       crass (~> 1.0.2) | ||||
|       nokogiri (>= 1.12.0) | ||||
|     mail (2.8.1) | ||||
| @ -192,111 +143,89 @@ GEM | ||||
|       net-pop | ||||
|       net-smtp | ||||
|     marcel (1.0.4) | ||||
|     method_source (1.1.0) | ||||
|     method_source (1.0.0) | ||||
|     mini_mime (1.1.5) | ||||
|     mini_portile2 (2.8.9) | ||||
|     minitest (5.25.5) | ||||
|     minitest (5.25.1) | ||||
|     money (6.19.0) | ||||
|       i18n (>= 0.6.4, <= 2) | ||||
|     msgpack (1.7.5) | ||||
|     multi_xml (0.7.1) | ||||
|       bigdecimal (~> 3.1) | ||||
|     net-imap (0.5.10) | ||||
|     msgpack (1.7.2) | ||||
|     net-imap (0.4.17) | ||||
|       date | ||||
|       net-protocol | ||||
|     net-pop (0.1.2) | ||||
|       net-protocol | ||||
|     net-protocol (0.2.2) | ||||
|       timeout | ||||
|     net-smtp (0.5.1) | ||||
|     net-smtp (0.5.0) | ||||
|       net-protocol | ||||
|     nio4r (2.7.4) | ||||
|     nokogiri (1.18.10) | ||||
|       mini_portile2 (~> 2.8.2) | ||||
|     nio4r (2.7.3) | ||||
|     nokogiri (1.16.7-aarch64-linux) | ||||
|       racc (~> 1.4) | ||||
|     nokogiri (1.18.10-aarch64-linux-gnu) | ||||
|     nokogiri (1.16.7-arm-linux) | ||||
|       racc (~> 1.4) | ||||
|     nokogiri (1.18.10-arm-linux-gnu) | ||||
|     nokogiri (1.16.7-arm64-darwin) | ||||
|       racc (~> 1.4) | ||||
|     nokogiri (1.18.10-arm64-darwin) | ||||
|     nokogiri (1.16.7-x86-linux) | ||||
|       racc (~> 1.4) | ||||
|     nokogiri (1.18.10-x86_64-darwin) | ||||
|     nokogiri (1.16.7-x86_64-darwin) | ||||
|       racc (~> 1.4) | ||||
|     nokogiri (1.18.10-x86_64-linux-gnu) | ||||
|     nokogiri (1.16.7-x86_64-linux) | ||||
|       racc (~> 1.4) | ||||
|     orm_adapter (0.5.0) | ||||
|     ostruct (0.6.2) | ||||
|     parallel (1.27.0) | ||||
|     parser (3.3.9.0) | ||||
|     parallel (1.26.3) | ||||
|     parser (3.3.5.0) | ||||
|       ast (~> 2.4.1) | ||||
|       racc | ||||
|     pg (1.6.2) | ||||
|     pg (1.6.2-aarch64-linux) | ||||
|     pg (1.6.2-arm64-darwin) | ||||
|     pg (1.6.2-x86_64-darwin) | ||||
|     pg (1.6.2-x86_64-linux) | ||||
|     pluck_to_hash (1.0.2) | ||||
|       activerecord (>= 4.0.2) | ||||
|       activesupport (>= 4.0.2) | ||||
|     pp (0.6.2) | ||||
|       prettyprint | ||||
|     prettyprint (0.2.0) | ||||
|     prism (1.5.1) | ||||
|     pry (0.15.2) | ||||
|     pg (1.5.9) | ||||
|     pry (0.14.2) | ||||
|       coderay (~> 1.1) | ||||
|       method_source (~> 1.0) | ||||
|     psych (5.2.6) | ||||
|       date | ||||
|     psych (5.1.2) | ||||
|       stringio | ||||
|     public_suffix (6.0.1) | ||||
|     puma (6.6.1) | ||||
|     puma (6.4.3) | ||||
|       nio4r (~> 2.0) | ||||
|     raabro (1.4.0) | ||||
|     racc (1.8.1) | ||||
|     rack (3.2.1) | ||||
|     rack-cors (3.0.0) | ||||
|       logger | ||||
|       rack (>= 3.0.14) | ||||
|     rack-session (2.1.1) | ||||
|       base64 (>= 0.1.0) | ||||
|     rack (3.1.8) | ||||
|     rack-cors (2.0.2) | ||||
|       rack (>= 2.0.0) | ||||
|     rack-session (2.0.0) | ||||
|       rack (>= 3.0.0) | ||||
|     rack-test (2.2.0) | ||||
|     rack-test (2.1.0) | ||||
|       rack (>= 1.3) | ||||
|     rackup (2.2.1) | ||||
|     rackup (2.1.0) | ||||
|       rack (>= 3) | ||||
|     rails (8.0.2.1) | ||||
|       actioncable (= 8.0.2.1) | ||||
|       actionmailbox (= 8.0.2.1) | ||||
|       actionmailer (= 8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       actiontext (= 8.0.2.1) | ||||
|       actionview (= 8.0.2.1) | ||||
|       activejob (= 8.0.2.1) | ||||
|       activemodel (= 8.0.2.1) | ||||
|       activerecord (= 8.0.2.1) | ||||
|       activestorage (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|       webrick (~> 1.8) | ||||
|     rails (7.2.1.2) | ||||
|       actioncable (= 7.2.1.2) | ||||
|       actionmailbox (= 7.2.1.2) | ||||
|       actionmailer (= 7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       actiontext (= 7.2.1.2) | ||||
|       actionview (= 7.2.1.2) | ||||
|       activejob (= 7.2.1.2) | ||||
|       activemodel (= 7.2.1.2) | ||||
|       activerecord (= 7.2.1.2) | ||||
|       activestorage (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       bundler (>= 1.15.0) | ||||
|       railties (= 8.0.2.1) | ||||
|     rails-dom-testing (2.3.0) | ||||
|       railties (= 7.2.1.2) | ||||
|     rails-dom-testing (2.2.0) | ||||
|       activesupport (>= 5.0.0) | ||||
|       minitest | ||||
|       nokogiri (>= 1.6) | ||||
|     rails-html-sanitizer (1.6.2) | ||||
|     rails-html-sanitizer (1.6.0) | ||||
|       loofah (~> 2.21) | ||||
|       nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) | ||||
|     railties (8.0.2.1) | ||||
|       actionpack (= 8.0.2.1) | ||||
|       activesupport (= 8.0.2.1) | ||||
|       nokogiri (~> 1.14) | ||||
|     railties (7.2.1.2) | ||||
|       actionpack (= 7.2.1.2) | ||||
|       activesupport (= 7.2.1.2) | ||||
|       irb (~> 1.13) | ||||
|       rackup (>= 1.0.0) | ||||
|       rake (>= 12.2) | ||||
|       thor (~> 1.0, >= 1.2.2) | ||||
|       zeitwerk (~> 2.6) | ||||
|     rainbow (3.1.1) | ||||
|     rake (13.3.0) | ||||
|     rdoc (6.14.2) | ||||
|       erb | ||||
|     rake (13.2.1) | ||||
|     rdoc (6.7.0) | ||||
|       psych (>= 4.0.0) | ||||
|     react-rails (3.2.1) | ||||
|       babel-transpiler (>= 0.7.0) | ||||
| @ -304,97 +233,46 @@ GEM | ||||
|       execjs | ||||
|       railties (>= 3.2) | ||||
|       tilt | ||||
|     redis (5.4.1) | ||||
|     redis (5.3.0) | ||||
|       redis-client (>= 0.22.0) | ||||
|     redis-client (0.23.2) | ||||
|     redis-client (0.22.2) | ||||
|       connection_pool | ||||
|     regexp_parser (2.11.3) | ||||
|     reline (0.6.2) | ||||
|     regexp_parser (2.9.2) | ||||
|     reline (0.5.10) | ||||
|       io-console (~> 0.5) | ||||
|     responders (3.1.1) | ||||
|       actionpack (>= 5.2) | ||||
|       railties (>= 5.2) | ||||
|     rexml (3.3.9) | ||||
|     rqrcode (3.1.0) | ||||
|       chunky_png (~> 1.0) | ||||
|       rqrcode_core (~> 2.0) | ||||
|     rqrcode_core (2.0.0) | ||||
|     rspec-core (3.13.5) | ||||
|     rspec-core (3.13.2) | ||||
|       rspec-support (~> 3.13.0) | ||||
|     rspec-expectations (3.13.5) | ||||
|     rspec-expectations (3.13.3) | ||||
|       diff-lcs (>= 1.2.0, < 2.0) | ||||
|       rspec-support (~> 3.13.0) | ||||
|     rspec-mocks (3.13.5) | ||||
|     rspec-mocks (3.13.2) | ||||
|       diff-lcs (>= 1.2.0, < 2.0) | ||||
|       rspec-support (~> 3.13.0) | ||||
|     rspec-rails (8.0.2) | ||||
|       actionpack (>= 7.2) | ||||
|       activesupport (>= 7.2) | ||||
|       railties (>= 7.2) | ||||
|     rspec-rails (7.0.1) | ||||
|       actionpack (>= 7.0) | ||||
|       activesupport (>= 7.0) | ||||
|       railties (>= 7.0) | ||||
|       rspec-core (~> 3.13) | ||||
|       rspec-expectations (~> 3.13) | ||||
|       rspec-mocks (~> 3.13) | ||||
|       rspec-support (~> 3.13) | ||||
|     rspec-support (3.13.5) | ||||
|     rswag (2.16.0) | ||||
|       rswag-api (= 2.16.0) | ||||
|       rswag-specs (= 2.16.0) | ||||
|       rswag-ui (= 2.16.0) | ||||
|     rswag-api (2.16.0) | ||||
|       activesupport (>= 5.2, < 8.1) | ||||
|       railties (>= 5.2, < 8.1) | ||||
|     rswag-specs (2.16.0) | ||||
|       activesupport (>= 5.2, < 8.1) | ||||
|       json-schema (>= 2.2, < 6.0) | ||||
|       railties (>= 5.2, < 8.1) | ||||
|       rspec-core (>= 2.14) | ||||
|     rswag-ui (2.16.0) | ||||
|       actionpack (>= 5.2, < 8.1) | ||||
|       railties (>= 5.2, < 8.1) | ||||
|     rubocop (1.80.2) | ||||
|     rspec-support (3.13.1) | ||||
|     rubocop (1.67.0) | ||||
|       json (~> 2.3) | ||||
|       language_server-protocol (~> 3.17.0.2) | ||||
|       lint_roller (~> 1.1.0) | ||||
|       language_server-protocol (>= 3.17.0) | ||||
|       parallel (~> 1.10) | ||||
|       parser (>= 3.3.0.2) | ||||
|       rainbow (>= 2.2.2, < 4.0) | ||||
|       regexp_parser (>= 2.9.3, < 3.0) | ||||
|       rubocop-ast (>= 1.46.0, < 2.0) | ||||
|       regexp_parser (>= 2.4, < 3.0) | ||||
|       rubocop-ast (>= 1.32.2, < 2.0) | ||||
|       ruby-progressbar (~> 1.7) | ||||
|       unicode-display_width (>= 2.4.0, < 4.0) | ||||
|     rubocop-ast (1.46.0) | ||||
|       parser (>= 3.3.7.2) | ||||
|       prism (~> 1.4) | ||||
|     rubocop-factory_bot (2.27.1) | ||||
|       lint_roller (~> 1.1) | ||||
|       rubocop (~> 1.72, >= 1.72.1) | ||||
|     rubocop-rails (2.33.3) | ||||
|       activesupport (>= 4.2.0) | ||||
|       lint_roller (~> 1.1) | ||||
|       rack (>= 1.1) | ||||
|       rubocop (>= 1.75.0, < 2.0) | ||||
|       rubocop-ast (>= 1.44.0, < 2.0) | ||||
|     rubocop-rspec (3.6.0) | ||||
|       lint_roller (~> 1.1) | ||||
|       rubocop (~> 1.72, >= 1.72.1) | ||||
|     rubocop-rspec_rails (2.31.0) | ||||
|       lint_roller (~> 1.1) | ||||
|       rubocop (~> 1.72, >= 1.72.1) | ||||
|       rubocop-rspec (~> 3.5) | ||||
|       unicode-display_width (>= 2.4.0, < 3.0) | ||||
|     rubocop-ast (1.32.3) | ||||
|       parser (>= 3.3.1.0) | ||||
|     ruby-progressbar (1.13.0) | ||||
|     rubytree (2.1.1) | ||||
|       json (~> 2.0, > 2.9) | ||||
|     rubyzip (2.3.2) | ||||
|     securerandom (0.4.1) | ||||
|     shoulda-matchers (6.5.0) | ||||
|       activesupport (>= 5.2.0) | ||||
|     solid_queue (1.2.1) | ||||
|       activejob (>= 7.1) | ||||
|       activerecord (>= 7.1) | ||||
|       concurrent-ruby (>= 1.3.1) | ||||
|       fugit (~> 1.11.0) | ||||
|       railties (>= 7.1) | ||||
|       thor (>= 1.3.1) | ||||
|     rubytree (2.1.0) | ||||
|       json (~> 2.0, > 2.3.1) | ||||
|     securerandom (0.3.1) | ||||
|     sprockets (4.2.1) | ||||
|       concurrent-ruby (~> 1.0) | ||||
|       rack (>= 2.2.4, < 4) | ||||
| @ -404,37 +282,27 @@ GEM | ||||
|       sprockets (>= 3.0.0) | ||||
|     stimulus-rails (1.3.4) | ||||
|       railties (>= 6.0.0) | ||||
|     stringio (3.1.7) | ||||
|     thor (1.4.0) | ||||
|     stringio (3.1.1) | ||||
|     thor (1.3.2) | ||||
|     tilt (2.4.0) | ||||
|     timeout (0.4.3) | ||||
|     tomlrb (2.0.3) | ||||
|     turbo-rails (2.0.16) | ||||
|       actionpack (>= 7.1.0) | ||||
|       railties (>= 7.1.0) | ||||
|     timeout (0.4.1) | ||||
|     turbo-rails (2.0.11) | ||||
|       actionpack (>= 6.0.0) | ||||
|       railties (>= 6.0.0) | ||||
|     tzinfo (2.0.6) | ||||
|       concurrent-ruby (~> 1.0) | ||||
|     unicode-display_width (2.6.0) | ||||
|     uri (1.0.3) | ||||
|     useragent (0.16.11) | ||||
|     warden (1.2.9) | ||||
|       rack (>= 2.0.9) | ||||
|     useragent (0.16.10) | ||||
|     web-console (4.2.1) | ||||
|       actionview (>= 6.0.0) | ||||
|       activemodel (>= 6.0.0) | ||||
|       bindex (>= 0.4.0) | ||||
|       railties (>= 6.0.0) | ||||
|     websocket-driver (0.7.7) | ||||
|       base64 | ||||
|     webrick (1.8.2) | ||||
|     websocket-driver (0.7.6) | ||||
|       websocket-extensions (>= 0.1.0) | ||||
|     websocket-extensions (0.1.5) | ||||
|     wicked_pdf (2.8.2) | ||||
|       activesupport | ||||
|       ostruct | ||||
|     with_env (1.1.0) | ||||
|     xml-simple (1.1.9) | ||||
|       rexml | ||||
|     zeitwerk (2.7.3) | ||||
|     zeitwerk (2.7.1) | ||||
| 
 | ||||
| PLATFORMS | ||||
|   aarch64-linux | ||||
| @ -445,222 +313,34 @@ PLATFORMS | ||||
|   x86_64-linux | ||||
| 
 | ||||
| DEPENDENCIES | ||||
|   acts_as_tenant | ||||
|   annotaterb | ||||
|   acts-as-taggable-on | ||||
|   bootsnap | ||||
|   chroma | ||||
|   csv | ||||
|   debug | ||||
|   devise (~> 4.9) | ||||
|   factory_bot_rails | ||||
|   faker | ||||
|   httparty | ||||
|   importmap-rails | ||||
|   jbuilder | ||||
|   jsonapi-rails | ||||
|   letter_opener_web | ||||
|   license_finder | ||||
|   money | ||||
|   pg (~> 1.1) | ||||
|   pluck_to_hash | ||||
|   pry | ||||
|   puma (>= 5.0) | ||||
|   rack-cors | ||||
|   rails (~> 8.0.0, >= 8.0.0) | ||||
|   rails (~> 7.2.0, >= 7.2.1) | ||||
|   react-rails | ||||
|   redis (>= 4.0.1) | ||||
|   rqrcode (~> 3.1) | ||||
|   rspec-rails (~> 8.0.0) | ||||
|   rswag | ||||
|   rspec-rails (~> 7.0.0) | ||||
|   rubocop | ||||
|   rubocop-factory_bot | ||||
|   rubocop-rails | ||||
|   rubocop-rspec | ||||
|   rubocop-rspec_rails | ||||
|   rubytree | ||||
|   shoulda-matchers (~> 6.0) | ||||
|   solid_queue (~> 1.0) | ||||
|   sprockets-rails | ||||
|   stimulus-rails | ||||
|   turbo-rails | ||||
|   tzinfo-data | ||||
|   web-console | ||||
|   wicked_pdf (~> 2.8) | ||||
| 
 | ||||
| CHECKSUMS | ||||
|   actioncable (8.0.2.1) sha256=6f1cb20db39fba28a93569e8d5dab42b2749d7ddd4baebb5bbecd4217e49d6a2 | ||||
|   actionmailbox (8.0.2.1) sha256=8ea8c6e31e448961c06fc1d6282775b32aff1c009f232d4564e07e54850a6cad | ||||
|   actionmailer (8.0.2.1) sha256=0de14d8d04541eab130858cb2f0697266be42de1afe1104bc43d7998137ddb9c | ||||
|   actionpack (8.0.2.1) sha256=61e7e11a31dbe5152ca57221788bdca42ef302c4cc53b4c8993d68dce8982b0a | ||||
|   actiontext (8.0.2.1) sha256=0cc4b3b5cfb9d915c6697b05b013dad7f4eaf074d9989700b6a0a55cf620d6b8 | ||||
|   actionview (8.0.2.1) sha256=2ea6d20ccb0b7b84a221a940ac06853ce99235e4ecb4947815839c7c5ecbf347 | ||||
|   activejob (8.0.2.1) sha256=d6e5f2da07ec8efac13a38af1752416770dc74e95783f7b252506d707aa32b89 | ||||
|   activemodel (8.0.2.1) sha256=17bab6cdb86531844113df22f864480a89a276bf0318246e628f99e0ac077ec4 | ||||
|   activerecord (8.0.2.1) sha256=a6556e7bdd53f3889d18d2aa3a7ff115fd6c5e1463dd06f97fb88d06b58c6df1 | ||||
|   activestorage (8.0.2.1) sha256=43bb3d9e115471e201e6a66813810c1d15b607a321f29d62efdf9d90ffaf76f8 | ||||
|   activesupport (8.0.2.1) sha256=0405a76fd1ca989975d9ae00d46a4d3979bdf3817482d846b63affa84bd561c6 | ||||
|   acts_as_tenant (1.0.1) sha256=6944e4d64533337938a8817a6b4ff9b11189c9dcc0b1333bb89f3821a4c14c53 | ||||
|   addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232 | ||||
|   annotaterb (4.19.0) sha256=c951df62059b3ac1ae383f4140bf935a140a15b6461f8d9a97d34b38ce2c7208 | ||||
|   ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 | ||||
|   babel-source (5.8.35) sha256=79ef222a9dcb867ac2efa3b0da35b4bcb15a4bfa67b6b2dcbf1e9a29104498d9 | ||||
|   babel-transpiler (0.7.0) sha256=4c06f4ad9e8e1cabe94f99e11df2f140bb72aca9ba067dbb49dc14d9b98d1570 | ||||
|   base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b | ||||
|   bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099 | ||||
|   benchmark (0.4.1) sha256=d4ef40037bba27f03b28013e219b950b82bace296549ec15a78016552f8d2cce | ||||
|   bigdecimal (3.2.3) sha256=ffd11d1ac67a0d3b2f44aec0a6487210b3f813f363dd11f1fcccf5ba00da4e1b | ||||
|   bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e | ||||
|   bootsnap (1.18.6) sha256=0ae2393c1e911e38be0f24e9173e7be570c3650128251bf06240046f84a07d00 | ||||
|   builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f | ||||
|   childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec | ||||
|   chroma (0.2.0) sha256=64bdcd36a4765fbcd45adc64960cc153101300b4918f90ffdd89f4e2eb954b54 | ||||
|   chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe | ||||
|   coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b | ||||
|   concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6 | ||||
|   connection_pool (2.5.4) sha256=e9e1922327416091f3f6542f5f4446c2a20745276b9aa796dd0bb2fd0ea1e70a | ||||
|   crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d | ||||
|   csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f | ||||
|   date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f | ||||
|   debug (1.11.0) sha256=1425db64cfa0130c952684e3dc974985be201dd62899bf4bbe3f8b5d6cf1aef2 | ||||
|   devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8 | ||||
|   diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 | ||||
|   drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 | ||||
|   erb (5.0.2) sha256=d30f258143d4300fb4ecf430042ac12970c9bb4b33c974a545b8f58c1ec26c0f | ||||
|   erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 | ||||
|   et-orbi (1.2.11) sha256=d26e868cc21db88280a9ec1a50aa3da5d267eb9b2037ba7b831d6c2731f5df64 | ||||
|   execjs (2.9.1) sha256=e8fd066f6df60c8e8fbebc32c6fb356b5212c77374e8416a9019ca4bb154dcfb | ||||
|   factory_bot (6.5.5) sha256=ce59295daee1b4704dab8a2fee6816f513d467c6aa3bc587860767d74a66efbe | ||||
|   factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 | ||||
|   faker (3.5.2) sha256=f9a80291b2e3f259801d1dd552f0732fe04dce5d1f74e798365bc0413789c473 | ||||
|   fugit (1.11.1) sha256=e89485e7be22226d8e9c6da411664d0660284b4b1c08cacb540f505907869868 | ||||
|   globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9 | ||||
|   httparty (0.23.1) sha256=3ac1dd62f2010f6ece551716f5ceec2b2012011d89f1751917ab7f724e966b55 | ||||
|   i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f | ||||
|   importmap-rails (2.2.2) sha256=729f5b1092f832780829ade1d0b46c7e53d91c556f06da7254da2977e93fe614 | ||||
|   io-console (0.8.1) sha256=1e15440a6b2f67b6ea496df7c474ed62c860ad11237f29b3bd187f054b925fcb | ||||
|   irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba | ||||
|   jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6 | ||||
|   json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68 | ||||
|   json-schema (5.0.1) sha256=bef71a82c600a42594911553522e143f7634affc198ed507ef3ded2f920a74a9 | ||||
|   jsonapi-deserializable (0.2.0) sha256=5f0ca2d3f8404cce1584a314e8a3753be32a56054c942adfe997b87e92bce147 | ||||
|   jsonapi-parser (0.1.1) sha256=9ee0dc031e88fc7548d56fab66f9716d1e1c06f972b529b8c4617bc42a097020 | ||||
|   jsonapi-rails (0.4.1) sha256=fa68b927b58f194e8b81f578c0bf18e61575638f45a390f66c832de2e6d179ba | ||||
|   jsonapi-rb (0.5.0) sha256=7922a164278f506c43d56277f6bd0800a0b603cc985f7f63fe7241b2628bd105 | ||||
|   jsonapi-renderer (0.2.2) sha256=b5c44b033d61b4abdb6500fa4ab84807ca0b36ea0e59e47a2c3ca7095a6e447b | ||||
|   jsonapi-serializable (0.3.1) sha256=221e657677659d798e268a33ec97a83ec5ea0e4233f931358db84e88056552e9 | ||||
|   language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc | ||||
|   launchy (3.0.1) sha256=b7fa60bda0197cf57614e271a250a8ca1f6a34ab889a3c73f67ec5d57c8a7f2c | ||||
|   letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2 | ||||
|   letter_opener_web (3.0.0) sha256=3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860 | ||||
|   license_finder (7.2.1) sha256=179ead19b64b170638b72fd16024233813673ac9d20d5ba75ae0b4444887ef14 | ||||
|   lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 | ||||
|   logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 | ||||
|   loofah (2.24.1) sha256=655a30842b70ec476410b347ab1cd2a5b92da46a19044357bbd9f401b009a337 | ||||
|   mail (2.8.1) sha256=ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad | ||||
|   marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4 | ||||
|   method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 | ||||
|   mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef | ||||
|   mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 | ||||
|   minitest (5.25.5) sha256=391b6c6cb43a4802bfb7c93af1ebe2ac66a210293f4a3fb7db36f2fc7dc2c756 | ||||
|   money (6.19.0) sha256=ec936fa1e42f2783719241ed9fd52725d0efa628f928feea1eb5c37d5de7daf3 | ||||
|   msgpack (1.7.5) sha256=ffb04979f51e6406823c03abe50e1da2c825c55a37dee138518cdd09d9d3aea8 | ||||
|   multi_xml (0.7.1) sha256=4fce100c68af588ff91b8ba90a0bb3f0466f06c909f21a32f4962059140ba61b | ||||
|   net-imap (0.5.10) sha256=f84d206a296bff48a3a10507567fc38b050d2a40c92ea0d448164f64e60d6205 | ||||
|   net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 | ||||
|   net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 | ||||
|   net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 | ||||
|   nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9 | ||||
|   nokogiri (1.18.10) sha256=d5cc0731008aa3b3a87b361203ea3d19b2069628cb55e46ac7d84a0445e69cc1 | ||||
|   nokogiri (1.18.10-aarch64-linux-gnu) sha256=7fb87235d729c74a2be635376d82b1d459230cc17c50300f8e4fcaabc6195344 | ||||
|   nokogiri (1.18.10-arm-linux-gnu) sha256=51f4f25ab5d5ba1012d6b16aad96b840a10b067b93f35af6a55a2c104a7ee322 | ||||
|   nokogiri (1.18.10-arm64-darwin) sha256=c2b0de30770f50b92c9323fa34a4e1cf5a0af322afcacd239cd66ee1c1b22c85 | ||||
|   nokogiri (1.18.10-x86_64-darwin) sha256=536e74bed6db2b5076769cab5e5f5af0cd1dccbbd75f1b3e1fa69d1f5c2d79e2 | ||||
|   nokogiri (1.18.10-x86_64-linux-gnu) sha256=ff5ba26ba2dbce5c04b9ea200777fd225061d7a3930548806f31db907e500f72 | ||||
|   orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 | ||||
|   ostruct (0.6.2) sha256=6d7302a299e400a2c248d6ce0dad18fc3a5714e8096facc25ffd0c54ee57cfc0 | ||||
|   parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 | ||||
|   parser (3.3.9.0) sha256=94d6929354b1a6e3e1f89d79d4d302cc8f5aa814431a6c9c7e0623335d7687f2 | ||||
|   pg (1.6.2) sha256=58614afd405cc9c2c9e15bffe8432e0d6cfc58b722344ad4a47c73a85189c875 | ||||
|   pg (1.6.2-aarch64-linux) sha256=0503c6be5b0ca5ca3aaf91f2ed638f90843313cb81e8e7d7b60ad4bb62c3d131 | ||||
|   pg (1.6.2-arm64-darwin) sha256=4d44500b28d5193b26674583d199a6484f80f1f2ea9cf54f7d7d06a1b7e316b6 | ||||
|   pg (1.6.2-x86_64-darwin) sha256=c441a55723584e2ae41749bf26024d7ffdfe1841b442308ed50cd6b7fda04115 | ||||
|   pg (1.6.2-x86_64-linux) sha256=525f438137f2d1411a1ebcc4208ec35cb526b5a3b285a629355c73208506a8ea | ||||
|   pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e | ||||
|   pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff | ||||
|   prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 | ||||
|   prism (1.5.1) sha256=b40c1b76ccb9fcccc3d1553967cda6e79fa7274d8bfea0d98b15d27a6d187134 | ||||
|   pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b | ||||
|   psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e | ||||
|   public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f | ||||
|   puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e | ||||
|   raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 | ||||
|   racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f | ||||
|   rack (3.2.1) sha256=30af3f7e5ec21b0d14d822cf24446048dba5f651b617c7e97405b604f20a9e33 | ||||
|   rack-cors (3.0.0) sha256=7b95be61db39606906b61b83bd7203fa802b0ceaaad8fcb2fef39e097bf53f68 | ||||
|   rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 | ||||
|   rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 | ||||
|   rackup (2.2.1) sha256=f737191fd5c5b348b7f0a4412a3b86383f88c43e13b8217b63d4c8d90b9e798d | ||||
|   rails (8.0.2.1) sha256=13ab95615569e74e364384b346b1d83e4795dbde83d9edf584e8768e8049b3ac | ||||
|   rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d | ||||
|   rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 | ||||
|   railties (8.0.2.1) sha256=54e40e1771fc2878f572d5a4e076cddb057ba8d4d471f8b7d9bfc61bc1301d4c | ||||
|   rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a | ||||
|   rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493 | ||||
|   rdoc (6.14.2) sha256=9fdd44df130f856ae70cc9a264dfd659b9b40de369b16581f4ab746e42439226 | ||||
|   react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64 | ||||
|   redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae | ||||
|   redis-client (0.23.2) sha256=e33bab6682c8155cfef95e6dd296936bb9c2981a89fb578ace27a076fa2836fa | ||||
|   regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 | ||||
|   reline (0.6.2) sha256=1dad26a6008872d59c8e05244b119347c9f2ddaf4a53dce97856cd5f30a02846 | ||||
|   responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a | ||||
|   rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9 | ||||
|   rqrcode (3.1.0) sha256=e2d5996375f6e9a013823c289ed575dbea678b8e0388574302c1fac563f098af | ||||
|   rqrcode_core (2.0.0) sha256=1e40b823ab57a96482a417fff5dd5c33645a00cea6ef5d9e342fecc5ef91d9ab | ||||
|   rspec-core (3.13.5) sha256=ab3f682897c6131c67f9a17cfee5022a597f283aebe654d329a565f9937a4fa3 | ||||
|   rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 | ||||
|   rspec-mocks (3.13.5) sha256=e4338a6f285ada9fe56f5893f5457783af8194f5d08884d17a87321d5195ea81 | ||||
|   rspec-rails (8.0.2) sha256=113139a53f5d068d4f48d1c29ad5f982013ed9b0daa69d7f7b266eda5d433ace | ||||
|   rspec-support (3.13.5) sha256=add745af535dd14b18f1209ab41ef987fdfad12786176b6a3b3619b9a7279fbf | ||||
|   rswag (2.16.0) sha256=f07ce41548b9bb51464c38bc7b95af22fee84b90f2d1197a515a623906353086 | ||||
|   rswag-api (2.16.0) sha256=b653f7bd92e98be18b01ab4525d88950d7b0960e293a99f856b9efcee3ae6074 | ||||
|   rswag-specs (2.16.0) sha256=8ba26085c408b0bd2ed21dc8015c80f417c7d34c63720ab7133c2549b5bd2a91 | ||||
|   rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f | ||||
|   rubocop (1.80.2) sha256=6485f30fefcf5c199db3b91e5e253b1ef43f7e564784e2315255809a3dd9abf4 | ||||
|   rubocop-ast (1.46.0) sha256=0da7f6ad5b98614f89b74f11873c191059c823eae07d6ffd40a42a3338f2232b | ||||
|   rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe | ||||
|   rubocop-rails (2.33.3) sha256=848c011b58c1292f3066246c9eb18abf6ffcfbce28bc57c4ab888bbec79af74b | ||||
|   rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479 | ||||
|   rubocop-rspec_rails (2.31.0) sha256=775375e18a26a1184a812ef3054b79d218e85601b9ae897f38f8be24dddf1f45 | ||||
|   ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 | ||||
|   rubytree (2.1.1) sha256=4925016356a81730e982f1f8c3b5f8da461f18906c77d238bad4c4ba896abd41 | ||||
|   rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f | ||||
|   securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 | ||||
|   shoulda-matchers (6.5.0) sha256=ef6b572b2bed1ac4aba6ab2c5ff345a24b6d055a93a3d1c3bfc86d9d499e3f44 | ||||
|   solid_queue (1.2.1) sha256=7976b3690a08080ef63d1b11281f0b77398f7697dbeda0e2c5532682639d4b15 | ||||
|   sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97 | ||||
|   sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e | ||||
|   stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 | ||||
|   stringio (3.1.7) sha256=5b78b7cb242a315fb4fca61a8255d62ec438f58da2b90be66048546ade4507fa | ||||
|   thor (1.4.0) sha256=8763e822ccb0f1d7bee88cde131b19a65606657b847cc7b7b4b82e772bcd8a3d | ||||
|   tilt (2.4.0) sha256=df74f29a451daed26591a85e8e0cebb198892cb75b6573394303acda273fba4d | ||||
|   timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e | ||||
|   tomlrb (2.0.3) sha256=c2736acf24919f793334023a4ff396c0647d93fce702a73c9d348deaa815d4f7 | ||||
|   turbo-rails (2.0.16) sha256=d24e1b60f0c575b3549ecda967e5391027143f8220d837ed792c8d48ea0ea38d | ||||
|   tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b | ||||
|   unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a | ||||
|   uri (1.0.3) sha256=e9f2244608eea2f7bc357d954c65c910ce0399ca5e18a7a29207ac22d8767011 | ||||
|   useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 | ||||
|   warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 | ||||
|   web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20 | ||||
|   websocket-driver (0.7.7) sha256=056d99f2cd545712cfb1291650fde7478e4f2661dc1db6a0fa3b966231a146b4 | ||||
|   websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 | ||||
|   wicked_pdf (2.8.2) sha256=648d9b0cec5a34adbc9bbf809731052a78119e2d6d323b9e4aa1383e1d683824 | ||||
|   with_env (1.1.0) sha256=50b3e4f0a6cda8f90d8a6bd87a6261f6c381429abafb161c4c69ad4a0cd0b6e4 | ||||
|   xml-simple (1.1.9) sha256=d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d | ||||
|   zeitwerk (2.7.3) sha256=b2e86b4a9b57d26ba68a15230dcc7fe6f040f06831ce64417b0621ad96ba3e85 | ||||
| 
 | ||||
| RUBY VERSION | ||||
|    ruby 3.4.3p32 | ||||
|    ruby 3.3.5p100 | ||||
| 
 | ||||
| BUNDLED WITH | ||||
|    2.6.1 | ||||
|    2.5.17 | ||||
|  | ||||
							
								
								
									
										104
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								README.md
									
									
									
									
									
								
							| @ -1,102 +1,24 @@ | ||||
| # Libre Wedding Planner | ||||
| # README | ||||
| 
 | ||||
| Libre Wedding Planner is Free, Open Source Software that helps organize several aspects of a wedding. | ||||
| This README would normally document whatever steps are necessary to get the | ||||
| application up and running. | ||||
| 
 | ||||
| The project is not production-ready yet. | ||||
| Things you may want to cover: | ||||
| 
 | ||||
| ## Features | ||||
| * Ruby version | ||||
| 
 | ||||
| The follwing features are either developed or under active development: | ||||
| * System dependencies | ||||
| 
 | ||||
| - Guests management | ||||
| - Expense management | ||||
| - Seating chart | ||||
| * Configuration | ||||
| 
 | ||||
| * Database creation | ||||
| 
 | ||||
| ## Next steps | ||||
| * Database initialization | ||||
| 
 | ||||
| Some ideas we would like to implement next: | ||||
| * How to run the test suite | ||||
| 
 | ||||
| - Authentication (required to make an instance public) | ||||
| - Website with wedding information | ||||
| - Attendance confirmation forms | ||||
| - Multitenancy | ||||
| * Services (job queues, cache servers, search engines, etc.) | ||||
| 
 | ||||
| # Development setup | ||||
| 
 | ||||
| Libre Wedding Planner is made of two main pieces: | ||||
| 
 | ||||
| - The backend (this repo), built with Ruby (on Rails) | ||||
| - The frontend (repo [here](https://gitea.bustikiller.com/bustikiller/wedding-planner-frontend/)), built with NextJS and React. You will need both to have the service fully working. | ||||
| 
 | ||||
| Both repositories are expected to live have a common parent directory: | ||||
| 
 | ||||
| ``` | ||||
| projects <or anything else> | ||||
|   |-> wedding-planner  | ||||
|   |-> wedding-planner-frontend  | ||||
| ``` | ||||
| 
 | ||||
| ## Docker compose | ||||
| 
 | ||||
| Docker compose is the recommended way to run Libre Wedding Planner for development purposes. After downloading both repositories, `cd` to the root of `wedding-planner` and run: | ||||
| 
 | ||||
| ```bash | ||||
| docker compose up --build | ||||
| ``` | ||||
| 
 | ||||
| Several containers will be started: | ||||
| 
 | ||||
| - backend: starts a Rails server that will act as an API. | ||||
| - workers: starts a runner of [solid queue](https://github.com/rails/solid_queue/) that takes .care of async tasks. | ||||
| - frontend: starts a NextJS application in charge of the frontend. | ||||
| - nginx: A reverse proxy that the backend and frontend under the same domain, and routes all requests to the upstream services. | ||||
| - db: A Postgres instance used by the backend service. | ||||
| 
 | ||||
| The backend service will seed the database with fake data. It's worth noting that the Postgres container does not have a volume, so the application will be seeded every time the container is created. | ||||
| 
 | ||||
| The backend, frontend and workers have hot-reloading enabled, so changes made to the codebase should be reflected in the application on the next request. | ||||
| 
 | ||||
| Please, include this in your `/etc/hosts` file: | ||||
| 
 | ||||
| ``` | ||||
| 127.0.0.1 libre-wedding-planner.app.localhost | ||||
| ``` | ||||
| 
 | ||||
| Once all containers have started, visit http://libre-wedding-planner.app.localhost/default/dashboard to load the application. | ||||
| 
 | ||||
| ## Multitenancy | ||||
| 
 | ||||
| LibreWeddingPlanner is designed to manage multiple weddings in a single host. All URLs (in the API and the frontend) are scoped under a slug that is unique per wedding. The slug is made of lowercase letters, numbers, and dashes (-). | ||||
| 
 | ||||
| The development environment is seeded with a wedding whose slug is `default`. | ||||
| ## Email delivery | ||||
| 
 | ||||
| In the development environment, real emails will not be sent. You can visit http://libre-wedding-planner.app.localhost/letter_opener/ to get a list of emails generated by the application. | ||||
| 
 | ||||
| ## Testing | ||||
| 
 | ||||
| Unit tests can be executed with | ||||
| 
 | ||||
| ``` | ||||
| bundle exec rspec | ||||
| ``` | ||||
| 
 | ||||
| ## API documentation | ||||
| 
 | ||||
| Generate the OpenAPI documentation with the command: | ||||
| 
 | ||||
| ``` | ||||
| rake rswag:specs:swaggerize | ||||
| ``` | ||||
| 
 | ||||
| The documentation is available in Swagger UI in http://libre-wedding-planner.app.localhost/api/api-docs/index.html. If testing the API through the UI, you will need to select the second server (which includes the `/api` path), intended for development. | ||||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
| Contributions of all kinds (code, UX/UI, testing, translations, etc.) are welcome. The procedures to contribute are still being defined, but don't hesitate to reach out in case you want to participate. | ||||
| 
 | ||||
| # License | ||||
| 
 | ||||
| This project is licensed under the GNU Affero General Public License (AGPL). Check [COPYING.md](./COPYING.md) for additional information. | ||||
| * Deployment instructions | ||||
| 
 | ||||
| * ... | ||||
|  | ||||
							
								
								
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Rakefile
									
									
									
									
									
								
							| @ -1,8 +1,6 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Add your own tasks in files placed in lib/tasks ending in .rake, | ||||
| # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. | ||||
| 
 | ||||
| require_relative 'config/application' | ||||
| require_relative "config/application" | ||||
| 
 | ||||
| Rails.application.load_tasks | ||||
|  | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ApplicationCable | ||||
|   class Channel < ActionCable::Channel::Base | ||||
|   end | ||||
|  | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ApplicationCable | ||||
|   class Connection < ActionCable::Connection::Base | ||||
|   end | ||||
|  | ||||
| @ -1,72 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AffinitiesController < ApplicationController | ||||
|   before_action :set_group, except: :reset | ||||
| 
 | ||||
|   def index | ||||
|     overridden = @group.affinities.each_with_object({}) do |affinity, acc| | ||||
|       acc[affinity.another_group(@group).id] = affinity.discomfort | ||||
|     end | ||||
| 
 | ||||
|     for_each_group do |group_id| | ||||
|       overridden[group_id] || GroupAffinity::NEUTRAL | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def bulk_update | ||||
|     affinities = params.expect(affinities: [%i[group_id affinity]]).map(&:to_h).map do |affinity| | ||||
|       { | ||||
|         group_a_id: @group.id, | ||||
|         group_b_id: affinity[:group_id], | ||||
|         discomfort: GroupAffinity::MAX_DISCOMFORT - affinity[:affinity] | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     GroupAffinity.upsert_all(affinities, unique_by: :uindex_group_pair) | ||||
| 
 | ||||
|     render json: {}, status: :ok | ||||
|   rescue ActiveRecord::InvalidForeignKey | ||||
|     render json: { error: 'At least one of the group IDs provided does not exist.' }, status: :bad_request | ||||
|   rescue ActiveRecord::StatementInvalid | ||||
|     render json: { error: 'Invalid group ID or discomfort provided.' }, status: :bad_request | ||||
|   end | ||||
| 
 | ||||
|   def default | ||||
|     hierarchy = AffinityGroupsHierarchy.new | ||||
| 
 | ||||
|     for_each_group do |group_id| | ||||
|       hierarchy.default_discomfort(@group.id, group_id).to_f | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def reset | ||||
|     hierarchy = AffinityGroupsHierarchy.new | ||||
| 
 | ||||
|     affinities = Group.pluck(:id).combination(2).map do |(group_a_id, group_b_id)| | ||||
|       { | ||||
|         group_a_id:, | ||||
|         group_b_id:, | ||||
|         discomfort: hierarchy.default_discomfort(group_a_id, group_b_id).to_f | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     GroupAffinity.upsert_all(affinities, unique_by: :uindex_group_pair) | ||||
| 
 | ||||
|     render json: {}, status: :ok | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def for_each_group | ||||
|     Group.where.not(id: @group.id) | ||||
|          .pluck(:id) | ||||
|          .index_with { |group_id| GroupAffinity::MAX_DISCOMFORT - yield(group_id) } | ||||
|          .then { |affinities| render json: affinities } | ||||
|   end | ||||
| 
 | ||||
|   def set_group | ||||
|     @group = Group.find(params[:group_id]) | ||||
|   end | ||||
| end | ||||
| @ -1,68 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ApplicationController < ActionController::Base | ||||
|   set_current_tenant_through_filter | ||||
|   before_action :set_tenant | ||||
|   before_action :authenticate_user! | ||||
|   after_action :set_csrf_cookie | ||||
| 
 | ||||
|   skip_before_action :verify_authenticity_token, if: :development_swagger? | ||||
| 
 | ||||
|   rescue_from ActiveRecord::RecordInvalid do |exception| | ||||
|     render json: { | ||||
|       message: 'Record invalid', | ||||
|       errors: exception.record.errors.full_messages | ||||
|     }, status: :unprocessable_entity | ||||
|   end | ||||
| 
 | ||||
|   rescue_from ActionController::ParameterMissing do |exception| | ||||
|     render json: { | ||||
|       message: 'Parameter missing', | ||||
|       errors: [exception.message] | ||||
|     }, status: :bad_request | ||||
|   end | ||||
| 
 | ||||
|   rescue_from ActiveRecord::RecordNotFound do |exception| | ||||
|     render json: { | ||||
|       message: 'Record not found', | ||||
|       errors: [exception.message] | ||||
|     }, status: :not_found | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def validate_captcha! | ||||
|     Rails.logger.info("Captcha params: #{captcha_params}") | ||||
| 
 | ||||
|     return if LibreCaptcha.new.valid?(id: captcha_params[:id], answer: captcha_params[:answer]) | ||||
| 
 | ||||
|     render json: { error: 'Incorrect CAPTCHA solution' }, status: :unprocessable_entity | ||||
|   end | ||||
| 
 | ||||
|   def captcha_params | ||||
|     params.expect(captcha: %i[id answer]) | ||||
|   end | ||||
| 
 | ||||
|   def default_url_options(options = {}) | ||||
|     options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug }) | ||||
|   end | ||||
| 
 | ||||
|   def set_tenant | ||||
|     set_current_tenant(Wedding.find_by!(slug: params[:slug])) | ||||
|   end | ||||
| 
 | ||||
|   def development_swagger? | ||||
|     Rails.env.test? || | ||||
|       (Rails.env.development? && request.headers['referer']&.include?('/api-docs/index.html')) | ||||
|   end | ||||
| 
 | ||||
|   def set_csrf_cookie | ||||
|     cookies['csrf-token'] = { | ||||
|       value: form_authenticity_token, | ||||
|       secure: false, | ||||
|       same_site: :strict | ||||
|     } | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class CaptchaController < ApplicationController | ||||
|   skip_before_action :authenticate_user! | ||||
|   skip_before_action :set_tenant | ||||
|   def create | ||||
|     id = LibreCaptcha.new.id | ||||
|     render json: { | ||||
|       id:, | ||||
|       media_url: media_captcha_index_url(id:) | ||||
|     }, status: :created | ||||
|   end | ||||
| end | ||||
| @ -1,34 +1,70 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ExpensesController < ApplicationController | ||||
|   def summary | ||||
|     render json: Expenses::TotalQuery.new.call | ||||
|   end | ||||
|   before_action :set_expense, only: %i[ show edit update destroy ] | ||||
| 
 | ||||
|   # GET /expenses or /expenses.json | ||||
|   def index | ||||
|     render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) | ||||
|     @expenses = Expense.all | ||||
|   end | ||||
| 
 | ||||
|   # GET /expenses/1 or /expenses/1.json | ||||
|   def show | ||||
|   end | ||||
| 
 | ||||
|   # GET /expenses/new | ||||
|   def new | ||||
|     @expense = Expense.new | ||||
|   end | ||||
| 
 | ||||
|   # GET /expenses/1/edit | ||||
|   def edit | ||||
|   end | ||||
| 
 | ||||
|   # POST /expenses or /expenses.json | ||||
|   def create | ||||
|     Expense.create!(expense_params) | ||||
|     render json: {}, status: :created | ||||
|     @expense = Expense.new(expense_params) | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       if @expense.save | ||||
|         format.html { redirect_to expense_url(@expense), notice: "Expense was successfully created." } | ||||
|         format.json { render :show, status: :created, location: @expense } | ||||
|       else | ||||
|         format.html { render :new, status: :unprocessable_entity } | ||||
|         format.json { render json: @expense.errors, status: :unprocessable_entity } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # PATCH/PUT /expenses/1 or /expenses/1.json | ||||
|   def update | ||||
|     Expense.find(params[:id]).update!(expense_params) | ||||
|     render json: {}, status: :ok | ||||
|     respond_to do |format| | ||||
|       if @expense.update(expense_params) | ||||
|         format.html { redirect_to expense_url(@expense), notice: "Expense was successfully updated." } | ||||
|         format.json { render :show, status: :ok, location: @expense } | ||||
|       else | ||||
|         format.html { render :edit, status: :unprocessable_entity } | ||||
|         format.json { render json: @expense.errors, status: :unprocessable_entity } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # DELETE /expenses/1 or /expenses/1.json | ||||
|   def destroy | ||||
|     Expense.find(params[:id]).destroy! | ||||
|     render json: {}, status: :ok | ||||
|     @expense.destroy! | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       format.html { redirect_to expenses_url, notice: "Expense was successfully destroyed." } | ||||
|       format.json { head :no_content } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|     # Use callbacks to share common setup or constraints between actions. | ||||
|     def set_expense | ||||
|       @expense = Expense.find(params[:id]) | ||||
|     end | ||||
| 
 | ||||
|   def expense_params | ||||
|     params.expect(expense: %i[name amount pricing_type]) | ||||
|   end | ||||
|     # Only allow a list of trusted parameters through. | ||||
|     def expense_params | ||||
|       params.require(:expense).permit(:name, :amount, :pricing_type) | ||||
|     end | ||||
| end | ||||
|  | ||||
| @ -1,46 +1,6 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class GroupsController < ApplicationController | ||||
|   def index | ||||
|     query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group| | ||||
|       { | ||||
|         id: group[:id], | ||||
|         name: group[:name], | ||||
|         icon: group[:icon], | ||||
|         color: group[:color], | ||||
|         parent_id: group[:parent_id], | ||||
|         attendance: group.slice(:total, :considered, :invited, :confirmed, :declined, :tentative) | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     render json: query_result | ||||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     group = Group.create!(**group_params, parent:) | ||||
|     render json: group.as_json(only: %i[id name icon color parent_id]), status: :created | ||||
|   end | ||||
| 
 | ||||
|   def update | ||||
|     group = Group.find(params[:id]) | ||||
|     group.update!(**group_params, parent:) | ||||
|     render json: group.as_json(only: %i[id name icon color parent_id]), status: :ok | ||||
|   end | ||||
| 
 | ||||
|   def destroy | ||||
|     Group.find(params[:id]).destroy! | ||||
|     render json: {}, status: :ok | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def parent | ||||
|     params[:group][:parent_id].present? ? Group.find(params[:group][:parent_id]) : nil | ||||
|   end | ||||
| 
 | ||||
|   def group_params | ||||
|     params.expect(group: %i[name icon color]) | ||||
|     roots = Group.where(parent_id: nil) | ||||
|     render jsonapi: roots, include: [children: [children: [:children]]] | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,45 +1,89 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'csv' | ||||
| 
 | ||||
| class GuestsController < ApplicationController | ||||
|   GUEST_PARAMS = { only: %i[id name status], include: { group: { only: %i[id name] } } }.freeze | ||||
| 
 | ||||
|   skip_before_action :authenticate_user!, only: :update | ||||
|   before_action :set_guest, only: %i[show edit update destroy] | ||||
| 
 | ||||
|   # GET /guests or /guests.json | ||||
|   def index | ||||
|     render json: Guest.includes(:group) | ||||
|                       .left_joins(:group) | ||||
|                       .order('groups.name' => :asc, invitation_id: :asc, name: :asc) | ||||
|                       .as_json(GUEST_PARAMS) | ||||
|     @guests = Guest.all | ||||
|                    .joins(:group) | ||||
|                    .order('groups.name' => :asc) | ||||
| 
 | ||||
|       render jsonapi: @guests | ||||
|   end | ||||
| 
 | ||||
|   # GET /guests/1 or /guests/1.json | ||||
|   def show; end | ||||
| 
 | ||||
|   # GET /guests/new | ||||
|   def new | ||||
|     @guest = Guest.new | ||||
|   end | ||||
| 
 | ||||
|   # GET /guests/1/edit | ||||
|   def edit; end | ||||
| 
 | ||||
|   # POST /guests or /guests.json | ||||
|   def create | ||||
|     guest = Guest.create!(guest_params) | ||||
|     render json: guest.as_json(GUEST_PARAMS), status: :created | ||||
|     @guest = Guest.new(guest_params) | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       if @guest.save | ||||
|         format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully created.' } | ||||
|         format.json { render :show, status: :created, location: @guest } | ||||
|       else | ||||
|         format.html { render :new, status: :unprocessable_entity } | ||||
|         format.json { render json: @guest.errors, status: :unprocessable_entity } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # PATCH/PUT /guests/1 or /guests/1.json | ||||
|   def update | ||||
|     guest = Guest.find(params[:id]) | ||||
|     guest.update!(guest_params) | ||||
|     respond_to do |format| | ||||
|       if @guest.update(guest_params) | ||||
|         format.html { redirect_to guest_url(@guest), notice: 'Guest was successfully updated.' } | ||||
|         format.json { render :show, status: :ok, location: @guest } | ||||
|       else | ||||
|         format.html { render :edit, status: :unprocessable_entity } | ||||
|         format.json { render json: @guest.errors, status: :unprocessable_entity } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|     if !user_signed_in? && guest.saved_change_to_status? | ||||
|       AdminMailer.with(guest_id: guest.id).attendance_change_email.deliver_later | ||||
|   # DELETE /guests/1 or /guests/1.json | ||||
|   def destroy | ||||
|     @guest.destroy! | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       format.html { redirect_to guests_url, notice: 'Guest was successfully destroyed.' } | ||||
|       format.json { head :no_content } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def import | ||||
|     csv = CSV.parse(params[:file].read, headers: true) | ||||
|     ActiveRecord::Base.transaction do | ||||
|       csv.each do |row| | ||||
|         guest = Guest.create!(first_name: row['name']) | ||||
| 
 | ||||
|         guest.affinity_group_list.add(row['affinity_group']) | ||||
|         guest.save! | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     render json: guest.as_json(GUEST_PARAMS), status: :ok | ||||
|   end | ||||
| 
 | ||||
|   def destroy | ||||
|     Guest.find(params[:id]).destroy! | ||||
|     render json: {}, status: :ok | ||||
|     redirect_to guests_url | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   # Use callbacks to share common setup or constraints between actions. | ||||
|   def set_guest | ||||
|     @guest = Guest.find(params[:id]) | ||||
|   end | ||||
| 
 | ||||
|   # Only allow a list of trusted parameters through. | ||||
|   def guest_params | ||||
|     user_signed_in? ? params.expect(guest: %i[name group_id status]) : params.expect(guest: %i[status]) | ||||
|     params.require(:guest).permit(:first_name, :last_name, :email, :phone) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,76 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class InvitationsController < ApplicationController | ||||
|   skip_before_action :authenticate_user!, only: :show | ||||
| 
 | ||||
|   def index | ||||
|     @invitations = Invitation.includes(:guests).all | ||||
|     respond_to do |format| | ||||
|       format.json do | ||||
|         render json: @invitations.as_json( | ||||
|           only: :id, | ||||
|           include: { guests: { only: %i[id name] } } | ||||
|         ) | ||||
|       end | ||||
|       format.pdf do | ||||
|         pdf_html = ActionController::Base.new.render_to_string( | ||||
|           template: 'invitations/sheet', | ||||
|           layout: 'pdf', | ||||
|           locals: { invitations: @invitations } | ||||
|         ) | ||||
|         pdf = WickedPdf.new.pdf_from_string(pdf_html) | ||||
|         send_data pdf, filename: "invitations_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def email | ||||
|     AdminMailer.with(wedding_id: ActsAsTenant.current_tenant.id).invitations_pdf_email.deliver_later | ||||
| 
 | ||||
|     head :ok | ||||
|   end | ||||
| 
 | ||||
|   def sheet; end | ||||
| 
 | ||||
|   def show | ||||
|     invitation = Invitation.includes(:guests).find(params[:id]) | ||||
| 
 | ||||
|     if invitation | ||||
|       render json: invitation, only: :id, include: { guests: { only: %i[id name status] } }, status: :ok | ||||
|     else | ||||
|       render json: { error: 'Invitation not found' }, status: :not_found | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     invitation = Invitation.create | ||||
| 
 | ||||
|     if invitation.persisted? | ||||
|       render json: invitation, only: :id, status: :created | ||||
|     else | ||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def update | ||||
|     invitation = Invitation.find(params[:id]) | ||||
| 
 | ||||
|     if invitation.update(guest_ids: params[:invitation][:guest_ids]) | ||||
|       render json: invitation, only: :id, include: { guests: { only: %i[id name] } }, status: :ok | ||||
|     else | ||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def destroy | ||||
|     invitation = Invitation.find(params[:id]) | ||||
| 
 | ||||
|     if invitation.destroy | ||||
|       head :no_content | ||||
|     else | ||||
|       render json: { errors: invitation.errors.full_messages }, status: :unprocessable_entity | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,44 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class SummaryController < ApplicationController | ||||
|   def index | ||||
|     render json: { | ||||
|       expenses:, | ||||
|       guests: | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def guests | ||||
|     guest_summary = Guest.group(:status).count | ||||
| 
 | ||||
|     { | ||||
|       total: guest_summary.except('considered').values.sum, | ||||
|       confirmed: guest_summary['confirmed'].to_i, | ||||
|       declined: guest_summary['declined'].to_i, | ||||
|       tentative: guest_summary['tentative'].to_i, | ||||
|       invited: guest_summary['invited'].to_i | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   def expenses | ||||
|     expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call | ||||
| 
 | ||||
|     { | ||||
|       projected: { | ||||
|         total: expense_summary['total_projected'], | ||||
|         guests: expense_summary['projected_guests'] | ||||
|       }, | ||||
|       confirmed: { | ||||
|         total: expense_summary['total_confirmed'], | ||||
|         guests: expense_summary['confirmed_guests'] | ||||
|       }, | ||||
|       status: { | ||||
|         paid: 0 | ||||
|       } | ||||
|     } | ||||
|   end | ||||
| end | ||||
| @ -1,57 +1,12 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class TablesArrangementsController < ApplicationController | ||||
|   def index | ||||
|     current_digest = Tables::Distribution.digest(current_tenant) | ||||
| 
 | ||||
|     render json: TablesArrangement | ||||
|       .order(valid: :desc) | ||||
|       .order(discomfort: :asc) | ||||
|       .select(:id, :name, :discomfort, :status, :progress) | ||||
|       .select("digest = '#{current_digest}'::uuid OR discomfort IS NULL as valid") | ||||
|       .limit(20) | ||||
|       .as_json(only: %i[id name discomfort valid status progress]) | ||||
|     @tables_arrangements = TablesArrangement.all.order(discomfort: :asc).limit(10) | ||||
|   end | ||||
| 
 | ||||
|   def show | ||||
|     Guest.joins(:seats, :group) | ||||
|          .where(seats: { tables_arrangement_id: params[:id] }) | ||||
|          .select('guests.*', 'groups.color', 'seats.table_number') | ||||
|          .group_by(&:table_number) | ||||
|          .map { |number, guests| format(number:, guests:) } | ||||
|          .then { |result| render json: { id: params[:id], tables: result } } | ||||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     ActiveRecord::Base.transaction do | ||||
|       tables_arrangement = TablesArrangement.create!(status: :not_started) | ||||
|       TableSimulatorJob.perform_later(current_tenant.id, tables_arrangement.id) | ||||
|     end | ||||
| 
 | ||||
|     render json: {}, status: :created | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def format(number:, guests:) | ||||
|     { | ||||
|       number: number, | ||||
|       discomfort: discomfort(guests: guests), | ||||
|       guests: guests.as_json(only: %i[id name color]) | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   def discomfort(guests:) | ||||
|     table = Tables::Table.new(guests) | ||||
| 
 | ||||
|     table.min_per_table = TableSimulatorJob::MIN_PER_TABLE | ||||
|     table.max_per_table = TableSimulatorJob::MAX_PER_TABLE | ||||
|     calculator = Tables::DiscomfortCalculator.new(table:) | ||||
|     { | ||||
|       discomfort: calculator.calculate, | ||||
|       breakdown: calculator.breakdown | ||||
|     } | ||||
|     @tables_arrangement = TablesArrangement.find(params[:id]) | ||||
|     @seats = @tables_arrangement.seats | ||||
|                                 .includes(guest: %i[affinity_groups unbreakable_bonds]) | ||||
|                                 .group_by(&:table_number) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,12 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class TokensController < ApplicationController | ||||
|   skip_before_action :authenticate_user! | ||||
|   skip_before_action :set_tenant | ||||
| 
 | ||||
|   def show | ||||
|     head :ok | ||||
|   end | ||||
| end | ||||
| @ -1,27 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Users | ||||
|   class ConfirmationsController < Devise::ConfirmationsController | ||||
|     clear_respond_to | ||||
|     respond_to :json | ||||
| 
 | ||||
|     def show | ||||
|       super do |resource| | ||||
|         if resource.errors.empty? | ||||
|           respond_to do |format| | ||||
|             format.json { render json: resource, status: :ok } | ||||
|             format.any { redirect_to root_path } | ||||
|           end | ||||
|         else | ||||
|           render json: { | ||||
|             message: 'Record invalid', | ||||
|             errors: resource.errors.full_messages | ||||
|           }, status: :unprocessable_entity | ||||
|         end | ||||
|         return | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,32 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Users | ||||
|   class RegistrationsController < Devise::RegistrationsController | ||||
|     clear_respond_to | ||||
|     respond_to :json | ||||
| 
 | ||||
|     before_action :validate_captcha!, only: :create | ||||
| 
 | ||||
|     def create | ||||
|       wedding = Wedding.create(slug: params[:slug]) | ||||
|       unless wedding.persisted? | ||||
|         render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity | ||||
|         return | ||||
|       end | ||||
| 
 | ||||
|       ActsAsTenant.with_tenant(wedding) do | ||||
|         super do |user| | ||||
|           wedding.destroy unless user.persisted? | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def set_tenant | ||||
|       set_current_tenant(nil) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,10 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Users | ||||
|   class SessionsController < Devise::SessionsController | ||||
|     clear_respond_to | ||||
|     respond_to :json | ||||
|   end | ||||
| end | ||||
| @ -1,25 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class WebsitesController < ApplicationController | ||||
|   skip_before_action :authenticate_user!, only: :show | ||||
| 
 | ||||
|   def show | ||||
|     render json: current_tenant.website.as_json(only: %i[content]) || {}, status: :ok | ||||
|   end | ||||
| 
 | ||||
|   def update | ||||
|     ActiveRecord::Base.transaction do | ||||
|       website = current_tenant.website || current_tenant.create_website | ||||
|       website.update!(website_params) | ||||
|       render json: website.as_json(only: %i[content]), status: :ok | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def website_params | ||||
|     params.expect(website: [:content]) | ||||
|   end | ||||
| end | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module TreeNodeExtension | ||||
|   def distance_to_common_ancestor(another_node) | ||||
|     return 0 if self == another_node | ||||
|  | ||||
| @ -1,6 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ApplicationHelper | ||||
| end | ||||
|  | ||||
| @ -1,6 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ExpensesHelper | ||||
| end | ||||
|  | ||||
| @ -1,6 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module GroupsHelper | ||||
| end | ||||
|  | ||||
| @ -1,6 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module GuestsHelper | ||||
| end | ||||
|  | ||||
| @ -1,6 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module TablesArrangementsHelper | ||||
| end | ||||
|  | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ApplicationJob < ActiveJob::Base | ||||
|   # Automatically retry jobs that encountered a deadlock | ||||
|   # retry_on ActiveRecord::Deadlocked | ||||
|  | ||||
| @ -1,51 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class TableSimulatorJob < ApplicationJob | ||||
|   queue_as :default | ||||
| 
 | ||||
|   MIN_PER_TABLE = 8 | ||||
|   MAX_PER_TABLE = 10 | ||||
| 
 | ||||
|   def perform(wedding_id, tables_arrangement_id) # rubocop:disable Metrics/MethodLength | ||||
|     Rails.logger.info "Starting table simulation #{tables_arrangement_id} for wedding #{wedding_id}" | ||||
|     ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do | ||||
|       engine = VNS::Engine.new | ||||
| 
 | ||||
|       engine.add_optimization(Tables::Swap) | ||||
|       engine.add_optimization(Tables::Shift) | ||||
| 
 | ||||
|       tables_arrangement = TablesArrangement.find(tables_arrangement_id) | ||||
| 
 | ||||
|       initial_solution = Tables::Distribution.new( | ||||
|         min_per_table: MIN_PER_TABLE, | ||||
|         max_per_table: MAX_PER_TABLE, | ||||
|         tables_arrangement_id: | ||||
|       ) | ||||
| 
 | ||||
|       initial_solution.random_distribution(Guest.potential.shuffle) | ||||
| 
 | ||||
|       initial_solution.save! | ||||
| 
 | ||||
|       engine.notify_progress do |current_progress| | ||||
|         tables_arrangement.update_columns(status: :in_progress, progress: current_progress) | ||||
|       end | ||||
| 
 | ||||
|       engine.on_better_solution do |better_solution| | ||||
|         better_solution.save! | ||||
|         tables_arrangement.update_columns(discomfort: better_solution.discomfort) # TODO: remove? | ||||
|       end | ||||
| 
 | ||||
|       engine.initial_solution = initial_solution | ||||
| 
 | ||||
|       engine.target_function(&:discomfort) | ||||
| 
 | ||||
|       best_solution = engine.run | ||||
| 
 | ||||
|       best_solution.save! | ||||
| 
 | ||||
|       tables_arrangement.update_columns(status: :completed) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,45 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AdminMailer < ApplicationMailer | ||||
|   def attendance_change_email | ||||
|     @guest = Guest.find(params[:guest_id]) | ||||
|     ActsAsTenant.with_tenant(@guest.wedding) do | ||||
|       mail( | ||||
|         to: recipients, | ||||
|         subject: I18n.t( | ||||
|           'admin_mailer.attendance_change_email.subject', | ||||
|           name: @guest.name, | ||||
|           status: I18n.t("active_record.attributes.guest/status.#{@guest.status}") | ||||
|         ) | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def invitations_pdf_email | ||||
|     ActsAsTenant.with_tenant(Wedding.find(params[:wedding_id])) do | ||||
|       invitations = Invitation.includes(:guests).all | ||||
| 
 | ||||
|       pdf_html = ActionController::Base.new.render_to_string( | ||||
|         template: 'invitations/sheet', | ||||
|         layout: 'pdf', | ||||
|         locals: { invitations: } | ||||
|       ) | ||||
|       pdf = WickedPdf.new.pdf_from_string(pdf_html) | ||||
| 
 | ||||
|       attachments["invitations_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf"] = pdf | ||||
| 
 | ||||
|       mail( | ||||
|         to: recipients, | ||||
|         subject: I18n.t('admin_mailer.invitations_pdf_email.subject') | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def recipients | ||||
|     ActsAsTenant.current_tenant.users.pluck(:email) | ||||
|   end | ||||
| end | ||||
| @ -1,18 +1,4 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ApplicationMailer < ActionMailer::Base | ||||
|   class << self | ||||
|     private | ||||
| 
 | ||||
|     def default_from | ||||
|       File.read('/run/secrets/smtp_user_name').strip | ||||
|     rescue Errno::ENOENT | ||||
|       'development@example.com' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   default from: default_from | ||||
|   layout 'mailer' | ||||
|   default from: "from@example.com" | ||||
|   layout "mailer" | ||||
| end | ||||
|  | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ApplicationRecord < ActiveRecord::Base | ||||
|   primary_abstract_class | ||||
| end | ||||
|  | ||||
| @ -1,34 +1,2 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: expenses | ||||
| # | ||||
| #  id           :uuid             not null, primary key | ||||
| #  amount       :decimal(, ) | ||||
| #  name         :string | ||||
| #  pricing_type :enum             default("fixed"), not null | ||||
| #  created_at   :datetime         not null | ||||
| #  updated_at   :datetime         not null | ||||
| #  wedding_id   :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_expenses_on_wedding_id  (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class Expense < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
|   enum :pricing_type, | ||||
|        fixed: 'fixed', | ||||
|        per_person: 'per_person' | ||||
| 
 | ||||
|   validates :name, presence: true | ||||
|   validates :amount, presence: true, numericality: { greater_than: 0 } | ||||
|   validates :pricing_type, presence: true | ||||
| end | ||||
|  | ||||
| @ -1,77 +1,9 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: groups | ||||
| # | ||||
| #  id         :uuid             not null, primary key | ||||
| #  color      :string | ||||
| #  icon       :string | ||||
| #  name       :string           not null | ||||
| #  order      :integer          default(1), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  parent_id  :uuid | ||||
| #  wedding_id :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_groups_on_name        (name) UNIQUE | ||||
| #  index_groups_on_parent_id   (parent_id) | ||||
| #  index_groups_on_wedding_id  (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (parent_id => groups.id) | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class Group < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
| 
 | ||||
|   validates :name, uniqueness: true | ||||
|   validates :name, :order, presence: true | ||||
| 
 | ||||
|   has_many :children, class_name: 'Group', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent | ||||
|   has_many :children, class_name: 'Group', foreign_key: 'parent_id' | ||||
|   belongs_to :parent, class_name: 'Group', optional: true | ||||
| 
 | ||||
|   before_create :set_color | ||||
| 
 | ||||
|   scope :roots, -> { where(parent_id: nil) } | ||||
| 
 | ||||
|   has_many :guests, dependent: :nullify | ||||
| 
 | ||||
|   def colorize_children(generation = 1) | ||||
|     children.zip(palette(generation)) do |child, raw_color| | ||||
|       final_color = raw_color.paint | ||||
|       final_color.brighten(60) if final_color.dark? | ||||
| 
 | ||||
|       child.update!(color: final_color) | ||||
| 
 | ||||
|       child.colorize_children(generation + 1) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def affinities | ||||
|     GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id)) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def palette(generation) | ||||
|     if generation == 1 | ||||
|       color.paint.palette.analogous(size: children.count) | ||||
|     else | ||||
|       color.paint.palette.decreasing_saturation | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def set_color | ||||
|     return if color.present? | ||||
| 
 | ||||
|     new_color = "##{SecureRandom.hex(3)}".paint | ||||
|     new_color = new_color.lighten(30) if new_color.dark? | ||||
|     self.color = new_color | ||||
|   end | ||||
|   has_many :guests | ||||
| end | ||||
|  | ||||
| @ -1,43 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: group_affinities | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  discomfort :float            not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  group_a_id :uuid             not null | ||||
| #  group_b_id :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_group_affinities_on_group_a_id  (group_a_id) | ||||
| #  index_group_affinities_on_group_b_id  (group_b_id) | ||||
| #  uindex_group_pair                     (LEAST(group_a_id, group_b_id), GREATEST(group_a_id, group_b_id)) UNIQUE | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (group_a_id => groups.id) | ||||
| #  fk_rails_...  (group_b_id => groups.id) | ||||
| # | ||||
| class GroupAffinity < ApplicationRecord | ||||
|   NEUTRAL = 1 | ||||
|   MIN_DISCOMFORT = 0 | ||||
|   MAX_DISCOMFORT = 2 | ||||
| 
 | ||||
|   belongs_to :group_a, class_name: 'Group' | ||||
|   belongs_to :group_b, class_name: 'Group' | ||||
| 
 | ||||
|   validates :discomfort, | ||||
|             numericality: { greater_than_or_equal_to: MIN_DISCOMFORT, less_than_or_equal_to: MAX_DISCOMFORT } | ||||
| 
 | ||||
|   def another_group(group) | ||||
|     return nil if group != group_a && group != group_b | ||||
| 
 | ||||
|     group == group_a ? group_b : group_a | ||||
|   end | ||||
| end | ||||
| @ -1,49 +1,16 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: guests | ||||
| # | ||||
| #  id            :uuid             not null, primary key | ||||
| #  name          :string | ||||
| #  phone         :string | ||||
| #  status        :integer          default("considered") | ||||
| #  created_at    :datetime         not null | ||||
| #  updated_at    :datetime         not null | ||||
| #  group_id      :uuid | ||||
| #  invitation_id :uuid | ||||
| #  wedding_id    :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_guests_on_group_id       (group_id) | ||||
| #  index_guests_on_invitation_id  (invitation_id) | ||||
| #  index_guests_on_wedding_id     (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (group_id => groups.id) | ||||
| #  fk_rails_...  (invitation_id => invitations.id) | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class Guest < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
|   belongs_to :group, optional: true | ||||
|   belongs_to :invitation, optional: true | ||||
|   acts_as_taggable_on :affinity_groups, :unbreakable_bonds | ||||
|   belongs_to :group | ||||
| 
 | ||||
|   enum :status, { | ||||
|   enum status: { | ||||
|     considered: 0, | ||||
|     invited: 10, | ||||
|     confirmed: 20, | ||||
|     declined: 30, | ||||
|     tentative: 40 | ||||
|   }, validate: true | ||||
|     tentative: 40, | ||||
|   } | ||||
| 
 | ||||
|   validates :name, presence: true | ||||
| 
 | ||||
|   scope :potential, -> { where.not(status: %i[declined considered]) } | ||||
| 
 | ||||
|   has_many :seats, dependent: :delete_all | ||||
|   def full_name | ||||
|     "#{first_name} #{last_name}" | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,29 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: invitations | ||||
| # | ||||
| #  id         :uuid             not null, primary key | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  wedding_id :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_invitations_on_wedding_id  (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class Invitation < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
|   has_many :guests, dependent: :nullify | ||||
| 
 | ||||
|   def url | ||||
|     "#{Rails.application.routes.url_helpers.root_url(slug: ActsAsTenant.current_tenant.slug)}/site/invitation/#{id}" | ||||
|   end | ||||
| end | ||||
| @ -1,33 +1,4 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: seats | ||||
| # | ||||
| #  id                    :uuid             not null, primary key | ||||
| #  table_number          :integer | ||||
| #  created_at            :datetime         not null | ||||
| #  updated_at            :datetime         not null | ||||
| #  guest_id              :uuid             not null | ||||
| #  tables_arrangement_id :uuid             not null | ||||
| #  wedding_id            :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_seats_on_guest_id               (guest_id) | ||||
| #  index_seats_on_tables_arrangement_id  (tables_arrangement_id) | ||||
| #  index_seats_on_wedding_id             (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (guest_id => guests.id) | ||||
| #  fk_rails_...  (tables_arrangement_id => tables_arrangements.id) ON DELETE => cascade | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class Seat < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
|   belongs_to :guest | ||||
|   belongs_to :tables_arrangement | ||||
|   belongs_to :table_arrangement | ||||
| end | ||||
|  | ||||
| @ -1,39 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: tables_arrangements | ||||
| # | ||||
| #  id         :uuid             not null, primary key | ||||
| #  digest     :uuid             not null | ||||
| #  discomfort :integer | ||||
| #  name       :string           not null | ||||
| #  progress   :float            default(0.0), not null | ||||
| #  status     :string           default("complete"), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  wedding_id :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_tables_arrangements_on_wedding_id  (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class TablesArrangement < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
|   has_many :seats, dependent: :delete_all | ||||
|   has_many :guests, through: :seats | ||||
| 
 | ||||
|   before_create :assign_name | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def assign_name | ||||
|     self.name = "#{Faker::Adjective.positive} #{Faker::Creature::Animal.name}".capitalize | ||||
|   end | ||||
|   has_many :seats | ||||
| end | ||||
|  | ||||
| @ -1,42 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: users | ||||
| # | ||||
| #  id                     :uuid             not null, primary key | ||||
| #  confirmation_sent_at   :datetime | ||||
| #  confirmation_token     :string | ||||
| #  confirmed_at           :datetime | ||||
| #  email                  :string           default(""), not null | ||||
| #  encrypted_password     :string           default(""), not null | ||||
| #  failed_attempts        :integer          default(0), not null | ||||
| #  locked_at              :datetime | ||||
| #  reset_password_sent_at :datetime | ||||
| #  reset_password_token   :string | ||||
| #  unconfirmed_email      :string | ||||
| #  unlock_token           :string | ||||
| #  created_at             :datetime         not null | ||||
| #  updated_at             :datetime         not null | ||||
| #  wedding_id             :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_users_on_confirmation_token    (confirmation_token) UNIQUE | ||||
| #  index_users_on_email                 (email) UNIQUE | ||||
| #  index_users_on_reset_password_token  (reset_password_token) UNIQUE | ||||
| #  index_users_on_unlock_token          (unlock_token) UNIQUE | ||||
| #  index_users_on_wedding_id            (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (wedding_id => weddings.id) ON DELETE => cascade | ||||
| # | ||||
| class User < ApplicationRecord | ||||
|   acts_as_tenant :wedding | ||||
| 
 | ||||
|   devise :database_authenticatable, :registerable, | ||||
|          :recoverable, :validatable, :confirmable, :lockable | ||||
| end | ||||
| @ -1,25 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: websites | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  content    :text | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  wedding_id :uuid             not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_websites_on_wedding_id  (wedding_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (wedding_id => weddings.id) | ||||
| # | ||||
| class Website < ApplicationRecord | ||||
|   belongs_to :wedding | ||||
| end | ||||
| @ -1,28 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: weddings | ||||
| # | ||||
| #  id         :uuid             not null, primary key | ||||
| #  slug       :string           not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_weddings_on_slug  (slug) UNIQUE | ||||
| # | ||||
| class Wedding < ApplicationRecord | ||||
|   SLUG_REGEX = /[a-z\d-]+/ | ||||
| 
 | ||||
|   validates :slug, presence: true, uniqueness: true, format: { with: /\A#{SLUG_REGEX}\z/ } | ||||
| 
 | ||||
|   has_many :guests, dependent: :delete_all | ||||
|   has_many :groups, dependent: :delete_all | ||||
|   has_many :invitations, dependent: :delete_all | ||||
|   has_many :users, dependent: :delete_all | ||||
|   has_one :website, dependent: :destroy | ||||
| end | ||||
| @ -1,50 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Expenses | ||||
|   class TotalQuery | ||||
|     private attr_reader :wedding | ||||
|     def initialize(wedding:) | ||||
|       @wedding = wedding | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       ActiveRecord::Base.connection.execute( | ||||
|         ActiveRecord::Base.sanitize_sql_array([query, { wedding_id: wedding.id }]) | ||||
|       ).first | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def query | ||||
|       <<~SQL.squish | ||||
|         WITH guest_count AS (#{guest_count_per_status}), | ||||
|              expense_summary AS (#{expense_summary}) | ||||
|         SELECT guest_count.confirmed as confirmed_guests, | ||||
|                guest_count.projected as projected_guests, | ||||
|                expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total_confirmed, | ||||
|                expense_summary.fixed + expense_summary.variable * guest_count.projected as total_projected | ||||
|         FROM guest_count, expense_summary; | ||||
|       SQL | ||||
|     end | ||||
| 
 | ||||
|     def expense_summary | ||||
|       <<~SQL.squish | ||||
|         SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, | ||||
|                coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable | ||||
|         FROM expenses | ||||
|         WHERE wedding_id = :wedding_id | ||||
|       SQL | ||||
|     end | ||||
| 
 | ||||
|     def guest_count_per_status | ||||
|       <<~SQL.squish | ||||
|         SELECT COALESCE(count(*) filter(where status = #{Guest.statuses['confirmed']}), 0) as confirmed, | ||||
|                COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected | ||||
|         FROM guests | ||||
|         WHERE wedding_id = :wedding_id | ||||
|       SQL | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,31 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Groups | ||||
|   class SummaryQuery | ||||
|     def call | ||||
|       Group.left_joins(:guests).group(:id).pluck_to_hash( | ||||
|         :id, | ||||
|         :name, | ||||
|         :icon, | ||||
|         :parent_id, | ||||
|         :color, | ||||
|         *count_expressions | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def count_expressions | ||||
|       [ | ||||
|         Arel.sql('count(*) filter (where status IS NOT NULL) as total'), | ||||
|         Arel.sql('count(*) filter (where status = 0) as considered'), | ||||
|         Arel.sql('count(*) filter (where status = 10) as invited'), | ||||
|         Arel.sql('count(*) filter (where status = 20) as confirmed'), | ||||
|         Arel.sql('count(*) filter (where status = 30) as declined'), | ||||
|         Arel.sql('count(*) filter (where status = 40) as tentative') | ||||
|       ] | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class SerializableGroup < JSONAPI::Serializable::Resource | ||||
|   type 'group' | ||||
| 
 | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class SerializableGuest < JSONAPI::Serializable::Resource | ||||
|   type 'guest' | ||||
| 
 | ||||
|   attributes :id, :status | ||||
|   attributes :id, :email, :group_id, :status | ||||
| 
 | ||||
|   attribute :name do | ||||
|     @object.name | ||||
|     "#{@object.first_name} #{@object.last_name}" | ||||
|   end | ||||
| 
 | ||||
|   attribute :group_name do | ||||
|     @object.group.name | ||||
|   end | ||||
| 
 | ||||
|   attribute :status do | ||||
|  | ||||
| @ -1,86 +1,27 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AffinityGroupsHierarchy < Array | ||||
|   DEFAULT_DISCOMFORT = 1 | ||||
|   include Singleton | ||||
| 
 | ||||
|   def initialize | ||||
|     super | ||||
|     @references = {} | ||||
| 
 | ||||
|     Group.roots.each do |group| | ||||
|       self << group.id | ||||
| 
 | ||||
|       hydrate(group) | ||||
|     end | ||||
| 
 | ||||
|     discomforts | ||||
|     invitation_counts | ||||
|     freeze | ||||
|   end | ||||
| 
 | ||||
|   def find(id) | ||||
|     @references[id] | ||||
|   def find(name) | ||||
|     @references[name] | ||||
|   end | ||||
| 
 | ||||
|   def <<(id) | ||||
|     new_node = Tree::TreeNode.new(id) | ||||
|     super(new_node).tap { @references[id] = new_node } | ||||
|   def <<(name) | ||||
|     new_node = Tree::TreeNode.new(name) | ||||
|     super(new_node).tap { @references[name] = new_node } | ||||
|   end | ||||
| 
 | ||||
|   def register_child(parent_id, child_id) | ||||
|     @references[parent_id] << Tree::TreeNode.new(child_id).tap { |child_node| @references[child_id] = child_node } | ||||
|   def register_child(parent_name, child_name) | ||||
|     @references[parent_name] << Tree::TreeNode.new(child_name).tap { |child_node| @references[child_name] = child_node } | ||||
|   end | ||||
| 
 | ||||
|   def distance(id_a, id_b) | ||||
|     return nil if @references[id_a].nil? || @references[id_b].nil? | ||||
|   def distance(name_a, name_b) | ||||
|     return nil if @references[name_a].nil? || @references[name_b].nil? | ||||
| 
 | ||||
|     @references[id_a].distance_to_common_ancestor(@references[id_b]) | ||||
|   end | ||||
| 
 | ||||
|   def discomfort(id_a, id_b) | ||||
|     return 0 if id_a == id_b | ||||
| 
 | ||||
|     @discomforts[uuid_to_int(id_a) + uuid_to_int(id_b)] || DEFAULT_DISCOMFORT | ||||
|   end | ||||
| 
 | ||||
|   def default_discomfort(id_a, id_b) | ||||
|     return 0 if id_a == id_b | ||||
| 
 | ||||
|     dist = distance(id_a, id_b) | ||||
| 
 | ||||
|     return DEFAULT_DISCOMFORT if dist.nil? | ||||
| 
 | ||||
|     Rational(dist, dist + 1) | ||||
|   end | ||||
| 
 | ||||
|   def guest_count(invitation_id) | ||||
|     @invitation_counts[invitation_id] || 0 | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def invitation_counts | ||||
|     @invitation_counts = Guest.where.not(invitation_id: nil).group(:invitation_id).count | ||||
|   end | ||||
| 
 | ||||
|   def discomforts | ||||
|     @discomforts ||= GroupAffinity.pluck(:group_a_id, :group_b_id, | ||||
|                                          :discomfort).each_with_object({}) do |(id_a, id_b, discomfort), acc| | ||||
|       acc[uuid_to_int(id_a) + uuid_to_int(id_b)] = discomfort | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def uuid_to_int(uuid) | ||||
|     uuid.gsub('-', '').hex | ||||
|   end | ||||
| 
 | ||||
|   def hydrate(group) | ||||
|     group.children.each do |child| | ||||
|       register_child(group.id, child.id) | ||||
| 
 | ||||
|       hydrate(child) | ||||
|     end | ||||
|     @references[name_a].distance_to_common_ancestor(@references[name_b]) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,20 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class LibreCaptcha | ||||
|   def id | ||||
|     HTTParty.post('http://libre-captcha:8888/v2/captcha', | ||||
|                   body: { | ||||
|                     input_type: 'text', | ||||
|                     level: :hard, | ||||
|                     media: 'image/png', | ||||
|                     size: '350x100' | ||||
|                   }.to_json).then { |raw| JSON.parse(raw)['id'] } | ||||
|   end | ||||
| 
 | ||||
|   def valid?(id:, answer:) | ||||
|     HTTParty.post('http://libre-captcha:8888/v2/answer', | ||||
|                   body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' } | ||||
|   end | ||||
| end | ||||
| @ -1,60 +1,24 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Tables | ||||
|   class DiscomfortCalculator | ||||
|     private attr_reader :table, :hierarchy | ||||
|     def initialize(table:, hierarchy: AffinityGroupsHierarchy.new) | ||||
|     private attr_reader :table | ||||
|     def initialize(table) | ||||
|       @table = table | ||||
|       @hierarchy = hierarchy | ||||
|     end | ||||
| 
 | ||||
|     def calculate | ||||
|       breakdown.values.sum | ||||
|     end | ||||
| 
 | ||||
|     def breakdown | ||||
|       @breakdown ||= { table_size_penalty:, cohesion_penalty:, invitations_penalty: } | ||||
|       cohesion_penalty | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     # | ||||
|     # Calculates the penalty associated with violating the table size constraints. The penalty is | ||||
|     # zero when the limits are honored, and it increases linearly as the number of guests deviates | ||||
|     # from the limits. Overcapacity is penalized more severely than undercapacity. | ||||
|     # | ||||
|     # @return [Number] The penalty associated with violating the table size constraints. | ||||
|     # | ||||
|     def table_size_penalty | ||||
|       case table.size | ||||
|       when 0...table.min_per_table then 5 * (table.min_per_table - table.size) | ||||
|       when table.min_per_table..table.max_per_table then 0 | ||||
|       else 5 * (table.size - table.max_per_table) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def cohesion_penalty | ||||
|       10 * (cohesion_discomfort * 1.0 / table.size) | ||||
|     end | ||||
|       table.map { |guest| guest.affinity_group_list.first }.tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| | ||||
|         distance = AffinityGroupsHierarchy.instance.distance(a, b) | ||||
| 
 | ||||
|     def invitations_penalty | ||||
|       2 * table.map(&:invitation_id) | ||||
|                .tally | ||||
|                .sum { |invitation_id, guests_in_table| hierarchy.guest_count(invitation_id) - guests_in_table } | ||||
|     end | ||||
|         next count_a * count_b if distance.nil? | ||||
|         next 0 if distance.zero? | ||||
| 
 | ||||
|     # | ||||
|     # Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort | ||||
|     # is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of | ||||
|     # guests is a rational number between 1 (unrelated groups) and 0 (same group). | ||||
|     # | ||||
|     # @return [Number] Total discomfort of the table. | ||||
|     # | ||||
|     def cohesion_discomfort | ||||
|       table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| | ||||
|         count_a * count_b * hierarchy.discomfort(a, b) | ||||
|         count_a * count_b * Rational(distance, distance + 1) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -1,35 +1,19 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require_relative '../../extensions/tree_node_extension' | ||||
| 
 | ||||
| module Tables | ||||
|   class Distribution | ||||
|     class << self | ||||
|       def digest(wedding) | ||||
|         Digest::UUID.uuid_v5(wedding.id, wedding.guests.potential.order(:id).pluck(:id).join) | ||||
|       end | ||||
|     end | ||||
|     attr_accessor :tables | ||||
| 
 | ||||
|     attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy, :tables_arrangement_id | ||||
| 
 | ||||
|     def initialize(min_per_table:, max_per_table:, tables_arrangement_id:, hierarchy: AffinityGroupsHierarchy.new) | ||||
|     def initialize(min_per_table:, max_per_table:) | ||||
|       @min_per_table = min_per_table | ||||
|       @max_per_table = max_per_table | ||||
|       @hierarchy = hierarchy | ||||
|       @tables = [] | ||||
|       @tables_arrangement_id = tables_arrangement_id | ||||
|     end | ||||
| 
 | ||||
|     def random_distribution(people, random: Random.new) | ||||
|       min_tables = (people.count * 1.0 / @max_per_table).ceil | ||||
|       max_tables = (people.count * 1.0 / @min_per_table).ceil | ||||
|       table_size = random.rand(min_tables..max_tables) | ||||
|       @tables = people.in_groups(table_size, false) | ||||
|                       .map { |group| Table.new(group) } | ||||
|                       .each { |table| table.min_per_table = @min_per_table } | ||||
|                       .each { |table| table.max_per_table = @max_per_table } | ||||
|     def random_distribution(people) | ||||
|       @tables = [] | ||||
| 
 | ||||
|       @tables << Table.new(people.slice!(0..rand(@min_per_table..@max_per_table))) while people.any? | ||||
|     end | ||||
| 
 | ||||
|     def discomfort | ||||
| @ -42,24 +26,21 @@ module Tables | ||||
|       "#{@tables.count} tables, discomfort: #{discomfort}" | ||||
|     end | ||||
| 
 | ||||
|     def pretty_print | ||||
|       @tables.map.with_index do |table, i| | ||||
|         "Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}" | ||||
|       end.join("\n") | ||||
|     end | ||||
| 
 | ||||
|     def deep_dup | ||||
|       self.class.new( | ||||
|         min_per_table: @min_per_table, | ||||
|         max_per_table: @max_per_table, | ||||
|         hierarchy: @hierarchy, | ||||
|         tables_arrangement_id: @tables_arrangement_id | ||||
|       ).tap do |new_distribution| | ||||
|       self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution| | ||||
|         new_distribution.tables = @tables.map(&:dup) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def save! | ||||
|       ActiveRecord::Base.transaction do | ||||
|         arrangement = TablesArrangement.find(tables_arrangement_id) | ||||
| 
 | ||||
|         self.tables_arrangement_id = arrangement.id | ||||
| 
 | ||||
|         arrangement.seats.delete_all | ||||
|         arrangement = TablesArrangement.create! | ||||
| 
 | ||||
|         records_to_store = [] | ||||
| 
 | ||||
| @ -71,17 +52,14 @@ module Tables | ||||
| 
 | ||||
|         Seat.insert_all!(records_to_store) | ||||
| 
 | ||||
|         arrangement.update!( | ||||
|           discomfort:, | ||||
|           digest: self.class.digest(tables.first.first.wedding) | ||||
|         ) | ||||
|         arrangement.update!(discomfort:) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def local_discomfort(table) | ||||
|       table.discomfort ||= DiscomfortCalculator.new(table:, hierarchy:).calculate | ||||
|       table.discomfort ||= DiscomfortCalculator.new(table).calculate | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,32 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Tables | ||||
|   class Shift | ||||
|     private attr_reader :initial_solution | ||||
|     def initialize(initial_solution) | ||||
|       @initial_solution = initial_solution | ||||
|     end | ||||
| 
 | ||||
|     def each | ||||
|       @initial_solution.tables.permutation(2) do |table_a, table_b| | ||||
|         table_a.dup.each do |person| | ||||
|           original_discomfort_a = table_a.reset | ||||
|           original_discomfort_b = table_b.reset | ||||
| 
 | ||||
|           table_a.delete(person) | ||||
|           table_b << person | ||||
| 
 | ||||
|           yield(@initial_solution) | ||||
|         ensure | ||||
|           table_b.delete(person) | ||||
|           table_a << person | ||||
| 
 | ||||
|           table_a.discomfort = original_discomfort_a | ||||
|           table_b.discomfort = original_discomfort_b | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,7 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Tables | ||||
|   class Swap | ||||
|     private attr_reader :initial_solution | ||||
| @ -11,7 +7,7 @@ module Tables | ||||
| 
 | ||||
|     def each | ||||
|       @initial_solution.tables.combination(2) do |table_a, table_b| | ||||
|         table_a.to_a.product(table_b.to_a).each do |(person_a, person_b)| | ||||
|         table_a.product(table_b).each do |(person_a, person_b)| | ||||
|           original_discomfort_a = table_a.reset | ||||
|           original_discomfort_b = table_b.reset | ||||
| 
 | ||||
|  | ||||
| @ -1,11 +1,6 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Tables | ||||
|   class Table < Set | ||||
|     attr_accessor :discomfort, :min_per_table, :max_per_table | ||||
| 
 | ||||
|   class Table < Array | ||||
|     attr_accessor :discomfort | ||||
|     def initialize(*args) | ||||
|       super | ||||
|       reset | ||||
| @ -17,4 +12,4 @@ module Tables | ||||
|       original_discomfort | ||||
|     end | ||||
|   end | ||||
| end | ||||
| end | ||||
| @ -1,33 +0,0 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Tables | ||||
|   class WheelSwap | ||||
|     private attr_reader :initial_solution | ||||
|     def initialize(initial_solution) | ||||
|       @initial_solution = initial_solution | ||||
|     end | ||||
| 
 | ||||
|     def call(size = 1) | ||||
|       Rails.logger.debug { "WheelSwap with size: #{size}" } | ||||
|       new_solution = @initial_solution.deep_dup | ||||
| 
 | ||||
|       selected_guests = [] | ||||
| 
 | ||||
|       size.times do | ||||
|         selected_guests += new_solution.tables.map(&:pop) | ||||
|       end | ||||
| 
 | ||||
|       selected_guests.shuffle! | ||||
| 
 | ||||
|       tables = new_solution.tables.cycle | ||||
| 
 | ||||
|       tables.next << selected_guests.pop while selected_guests.any? | ||||
| 
 | ||||
|       new_solution.tables.each(&:reset) | ||||
| 
 | ||||
|       new_solution | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,112 +1,47 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module VNS | ||||
|   class Engine | ||||
|     PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze | ||||
|     ITERATIONS = 50 | ||||
|     class << self | ||||
|       def sequence(elements) | ||||
|         elements = elements.to_a | ||||
|         (elements + elements.reverse).chunk(&:itself).map(&:first) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def initialize | ||||
|       @perturbations = Set.new | ||||
|     end | ||||
| 
 | ||||
|     def target_function(&function) | ||||
|       @target_function = function | ||||
|     end | ||||
| 
 | ||||
|     def add_optimization(klass) | ||||
|       @optimizations ||= Set.new | ||||
|       @optimizations << klass | ||||
|     end | ||||
| 
 | ||||
|     def add_perturbation(klass) | ||||
|       @perturbations ||= Set.new | ||||
|       @perturbations << klass | ||||
|     end | ||||
| 
 | ||||
|     def notify_progress(&block) | ||||
|       @progress_notifier = block | ||||
|     end | ||||
| 
 | ||||
|     def on_better_solution(&block) | ||||
|       @better_solution_notifier = block | ||||
|     end | ||||
| 
 | ||||
|     attr_writer :initial_solution | ||||
| 
 | ||||
|     def run | ||||
|       check_preconditions! | ||||
|       raise 'No target function defined' unless @target_function | ||||
|       raise 'No perturbations defined' unless @perturbations | ||||
|       raise 'No initial solution defined' unless @initial_solution | ||||
| 
 | ||||
|       @current_solution = @initial_solution | ||||
|       @best_score = @target_function.call(@current_solution) | ||||
|       @best_solution = @initial_solution | ||||
|       @best_score = @target_function.call(@best_solution) | ||||
| 
 | ||||
|       run_all_optimizations | ||||
|       puts "Initial score: #{@best_score.to_f}" | ||||
| 
 | ||||
|       @progress_notifier&.call(Rational(1, ITERATIONS + 1)) | ||||
| 
 | ||||
|       best_solution = @current_solution | ||||
| 
 | ||||
|       (1..ITERATIONS).each do |iteration| | ||||
|         @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample) | ||||
|         @best_score = @target_function.call(@current_solution) | ||||
|         Rails.logger.debug { "After perturbation: #{@best_score}" } | ||||
| 
 | ||||
|         run_all_optimizations | ||||
| 
 | ||||
|         @progress_notifier&.call(Rational(iteration + 1, ITERATIONS + 1)) | ||||
| 
 | ||||
|         next unless best_solution.discomfort > @current_solution.discomfort | ||||
| 
 | ||||
|         best_solution = @current_solution | ||||
|         @better_solution_notifier&.call(best_solution) | ||||
| 
 | ||||
|         Rails.logger.debug do | ||||
|           "Found better solution after perturbation optimization: #{@current_solution.discomfort}" | ||||
|         end | ||||
|       @perturbations.each do |perturbation| | ||||
|         puts "Running perturbation: #{perturbation.name}" | ||||
|         optimize(perturbation.new(@best_solution)) | ||||
|       end | ||||
| 
 | ||||
|       best_solution | ||||
|       @best_solution  | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def check_preconditions! | ||||
|       raise 'No target function defined' unless @target_function | ||||
|       raise 'No optimizations defined' unless @optimizations | ||||
|       raise 'No initial solution defined' unless @initial_solution | ||||
|     end | ||||
|     def optimize(perturbation) | ||||
|       perturbation.each do |alternative_solution| | ||||
|         score = @target_function.call(alternative_solution) | ||||
|         next if score >= @best_score | ||||
| 
 | ||||
|     def run_all_optimizations | ||||
|       self.class.sequence(@optimizations).each do |optimization| | ||||
|         optimize(optimization) | ||||
|         Rails.logger.debug { "Finished optimization phase: #{optimization}" } | ||||
|       end | ||||
|       Rails.logger.debug { 'Finished all optimization phases' } | ||||
|     end | ||||
|         @best_solution = alternative_solution.deep_dup | ||||
|         @best_score = score | ||||
| 
 | ||||
|     def optimize(optimization_klass) | ||||
|       loop do | ||||
|         optimized = false | ||||
|         puts "New lowest score: #{@best_score.to_f}" | ||||
| 
 | ||||
|         optimization_klass.new(@current_solution).each do |alternative_solution| | ||||
|           score = @target_function.call(alternative_solution) | ||||
|           next if score >= @best_score | ||||
| 
 | ||||
|           @current_solution = alternative_solution.deep_dup | ||||
|           @best_score = score | ||||
|           optimized = true | ||||
|           Rails.logger.debug { "[#{optimization_klass}] Found better solution with score: #{score}" } | ||||
| 
 | ||||
|           break | ||||
|         end | ||||
| 
 | ||||
|         return unless optimized | ||||
|         return optimize(perturbation.class.new(@best_solution)) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -1,17 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p><%= I18n.t('admin_mailer.greeting') %>,</p> | ||||
| 
 | ||||
| <p> | ||||
|   <%= I18n.t('admin_mailer.attendance_change_email.paragraph_1', name: @guest.name) %> | ||||
| </p> | ||||
| 
 | ||||
| <ul> | ||||
|   <li> | ||||
|     <strong><%= I18n.t("active_record.attributes.guest.status") %>:</strong> <%= I18n.t("active_record.attributes.guest/status.#{@guest.status}") %> | ||||
|   </li> | ||||
| </ul> | ||||
| 
 | ||||
| <p> | ||||
|   <%= I18n.t("admin_mailer.attendance_change_email.notify_on_updates") %> | ||||
| </p> | ||||
| @ -1,9 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <%= I18n.t('admin_mailer.greeting') %>, | ||||
| 
 | ||||
| <%= I18n.t('admin_mailer.attendance_change_email.paragraph_1', name: @guest.name) %> | ||||
| 
 | ||||
| - <%= I18n.t("active_record.attributes.guest.status") %>: <%= I18n.t("active_record.attributes.guest/status.#{@guest.status}") %> | ||||
| 
 | ||||
| <%= I18n.t("admin_mailer.attendance_change_email.notify_on_updates") %> | ||||
| @ -1,7 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p><%= I18n.t('admin_mailer.greeting') %>,</p> | ||||
| 
 | ||||
| <p> | ||||
|   <%= I18n.t('admin_mailer.invitations_pdf_email.paragraph_1') %> | ||||
| </p> | ||||
| @ -1,5 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <%= I18n.t('admin_mailer.greeting') %>, | ||||
| 
 | ||||
| <%= I18n.t('admin_mailer.invitations_pdf_email.paragraph_1') %> | ||||
							
								
								
									
										17
									
								
								app/views/expenses/_expense.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/views/expenses/_expense.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <div id="<%= dom_id expense %>"> | ||||
|   <p> | ||||
|     <strong>Name:</strong> | ||||
|     <%= expense.name %> | ||||
|   </p> | ||||
| 
 | ||||
|   <p> | ||||
|     <strong>Amount:</strong> | ||||
|     <%= expense.amount %> | ||||
|   </p> | ||||
| 
 | ||||
|   <p> | ||||
|     <strong>Pricing type:</strong> | ||||
|     <%= expense.pricing_type %> | ||||
|   </p> | ||||
| 
 | ||||
| </div> | ||||
							
								
								
									
										32
									
								
								app/views/expenses/_form.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/views/expenses/_form.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| <%= form_with(model: expense) do |form| %> | ||||
|   <% if expense.errors.any? %> | ||||
|     <div style="color: red"> | ||||
|       <h2><%= pluralize(expense.errors.count, "error") %> prohibited this expense from being saved:</h2> | ||||
| 
 | ||||
|       <ul> | ||||
|         <% expense.errors.each do |error| %> | ||||
|           <li><%= error.full_message %></li> | ||||
|         <% end %> | ||||
|       </ul> | ||||
|     </div> | ||||
|   <% end %> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :name, style: "display: block" %> | ||||
|     <%= form.text_field :name %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :amount, style: "display: block" %> | ||||
|     <%= form.text_field :amount %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :pricing_type, style: "display: block" %> | ||||
|     <%= form.text_field :pricing_type %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.submit %> | ||||
|   </div> | ||||
| <% end %> | ||||
							
								
								
									
										10
									
								
								app/views/expenses/edit.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/views/expenses/edit.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <h1>Editing expense</h1> | ||||
| 
 | ||||
| <%= render "form", expense: @expense %> | ||||
| 
 | ||||
| <br> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Show this expense", @expense %> | | ||||
|   <%= link_to "Back to expenses", expenses_path %> | ||||
| </div> | ||||
							
								
								
									
										28
									
								
								app/views/expenses/index.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/views/expenses/index.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| <p style="color: green"><%= notice %></p> | ||||
| 
 | ||||
| <h1>Expenses</h1> | ||||
| 
 | ||||
| <div id="expenses"> | ||||
|   <table> | ||||
|     <tr> | ||||
|       <th>Name</th> | ||||
|       <th>Amount</th> | ||||
|       <th colspan="2"></th> | ||||
|     </tr> | ||||
|   <% @expenses.each do |expense| %> | ||||
|     <tr> | ||||
|       <td><%= expense.name %></td> | ||||
|       <td><%= expense.amount.to_currency %></td> | ||||
|       <td><%= link_to "Show", expense %></td> | ||||
|       <td><%= link_to "Edit", edit_expense_path(expense) %></td> | ||||
|     </tr> | ||||
|   <% end %> | ||||
|   <tr> | ||||
|     <td>Total</td> | ||||
|     <td><%= @expenses.sum(&:amount).to_currency %></td> | ||||
|     <td colspan="2"></td> | ||||
|   </tr> | ||||
|   </table> | ||||
| </div> | ||||
| 
 | ||||
| <%= link_to "New expense", new_expense_path %> | ||||
							
								
								
									
										9
									
								
								app/views/expenses/new.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/expenses/new.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <h1>New expense</h1> | ||||
| 
 | ||||
| <%= render "form", expense: @expense %> | ||||
| 
 | ||||
| <br> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Back to expenses", expenses_path %> | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								app/views/expenses/show.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/views/expenses/show.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <p style="color: green"><%= notice %></p> | ||||
| 
 | ||||
| <%= render @expense %> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Edit this expense", edit_expense_path(@expense) %> | | ||||
|   <%= link_to "Back to expenses", expenses_path %> | ||||
| 
 | ||||
|   <%= button_to "Destroy this expense", @expense, method: :delete %> | ||||
| </div> | ||||
							
								
								
									
										37
									
								
								app/views/guests/_form.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/views/guests/_form.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| <%= form_with(model: guest) do |form| %> | ||||
|   <% if guest.errors.any? %> | ||||
|     <div style="color: red"> | ||||
|       <h2><%= pluralize(guest.errors.count, "error") %> prohibited this guest from being saved:</h2> | ||||
| 
 | ||||
|       <ul> | ||||
|         <% guest.errors.each do |error| %> | ||||
|           <li><%= error.full_message %></li> | ||||
|         <% end %> | ||||
|       </ul> | ||||
|     </div> | ||||
|   <% end %> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :first_name, style: "display: block" %> | ||||
|     <%= form.text_field :first_name %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :last_name, style: "display: block" %> | ||||
|     <%= form.text_field :last_name %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :email, style: "display: block" %> | ||||
|     <%= form.text_field :email %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.label :phone, style: "display: block" %> | ||||
|     <%= form.text_field :phone %> | ||||
|   </div> | ||||
| 
 | ||||
|   <div> | ||||
|     <%= form.submit %> | ||||
|   </div> | ||||
| <% end %> | ||||
							
								
								
									
										22
									
								
								app/views/guests/_guest.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/views/guests/_guest.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| <div id="<%= dom_id guest %>"> | ||||
|   <p> | ||||
|     <strong>First name:</strong> | ||||
|     <%= guest.first_name %> | ||||
|   </p> | ||||
| 
 | ||||
|   <p> | ||||
|     <strong>Last name:</strong> | ||||
|     <%= guest.last_name %> | ||||
|   </p> | ||||
| 
 | ||||
|   <p> | ||||
|     <strong>Email:</strong> | ||||
|     <%= guest.email %> | ||||
|   </p> | ||||
| 
 | ||||
|   <p> | ||||
|     <strong>Phone:</strong> | ||||
|     <%= guest.phone %> | ||||
|   </p> | ||||
| 
 | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								app/views/guests/edit.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/views/guests/edit.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <h1>Editing guest</h1> | ||||
| 
 | ||||
| <%= render "form", guest: @guest %> | ||||
| 
 | ||||
| <br> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Show this guest", @guest %> | | ||||
|   <%= link_to "Back to guests", guests_path %> | ||||
| </div> | ||||
							
								
								
									
										37
									
								
								app/views/guests/index.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/views/guests/index.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| <p style="color: green"><%= notice %></p> | ||||
| 
 | ||||
| <h1>Guests</h1> | ||||
| 
 | ||||
| <div id="guests"> | ||||
|   <table> | ||||
|     <tr> | ||||
|       <th>Row #</th> | ||||
|       <th>Name</th> | ||||
|       <th>Email</th> | ||||
|       <th>Phone</th> | ||||
|       <th>Affinity groups</th> | ||||
|       <th>Unbreakable bonds</th> | ||||
|       <th colspan="2"></th> | ||||
|     </tr> | ||||
|   <% @guests.each_with_index do |guest, i| %> | ||||
|     <tr> | ||||
|       <td><%= i + 1 %></td> | ||||
|       <td><%= guest.full_name %></td> | ||||
|       <td><%= guest.email %></td> | ||||
|       <td><%= guest.phone %></td> | ||||
|       <td><%= guest.affinity_groups.pluck(:name).join(", ") %></td> | ||||
|       <td><%= guest.unbreakable_bonds.pluck(:name).join(", ") %></td> | ||||
|       <td><%= link_to "Show", guest %></td> | ||||
|       <td><%= link_to "Edit", edit_guest_path(guest) %></td> | ||||
|     </tr> | ||||
|   <% end %> | ||||
|   </table> | ||||
| </div> | ||||
| 
 | ||||
| <%= link_to "New guest", new_guest_path %> | ||||
| 
 | ||||
| <%= form_with url: import_guests_path, method: :post do |form| %> | ||||
|   <%= form.label :file %> | ||||
|   <%= form.file_field :file %> | ||||
|   <%= form.submit "Import" %> | ||||
| <% end %> | ||||
							
								
								
									
										9
									
								
								app/views/guests/new.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/guests/new.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <h1>New guest</h1> | ||||
| 
 | ||||
| <%= render "form", guest: @guest %> | ||||
| 
 | ||||
| <br> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Back to guests", guests_path %> | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								app/views/guests/show.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/views/guests/show.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <p style="color: green"><%= notice %></p> | ||||
| 
 | ||||
| <%= render @guest %> | ||||
| 
 | ||||
| <div> | ||||
|   <%= link_to "Edit this guest", edit_guest_path(@guest) %> | | ||||
|   <%= link_to "Back to guests", guests_path %> | ||||
| 
 | ||||
|   <%= button_to "Destroy this guest", @guest, method: :delete %> | ||||
| </div> | ||||
| @ -1,33 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <% invitations.each_slice(4) do |invitation_group| %> | ||||
|   <table style="width: 100%; border-collapse: separate; border-spacing: 0 20px; margin-bottom: 40px;"> | ||||
|     <% invitation_group.each do |invitation| %> | ||||
|       <tr> | ||||
|         <td style="width: 270px; height: 270px; text-align: center; vertical-align: middle; padding: 10px;"> | ||||
|           <%= image_tag(RQRCode::QRCode.new(invitation.url).as_png( | ||||
|                 bit_depth: 1, | ||||
|                 border_modules: 4, | ||||
|                 color_mode: ChunkyPNG::COLOR_GRAYSCALE, | ||||
|                 color: "black", | ||||
|                 file: nil, | ||||
|                 fill: "white", | ||||
|                 module_px_size: 6, | ||||
|                 resize_exactly_to: false, | ||||
|                 resize_gte_to: false, | ||||
|                 size: 250 | ||||
|               ).to_data_url) | ||||
|           %> | ||||
|         </td> | ||||
|         <td style="vertical-align: middle; padding: 10px;"> | ||||
|           <ul style="margin: 0; padding-left: 20px;"> | ||||
|             <% invitation.guests.each do |guest| %> | ||||
|               <%= content_tag(:li, guest.name) %> | ||||
|             <% end %> | ||||
|           </ul> | ||||
|         </td> | ||||
|       </tr> | ||||
|     <% end %> | ||||
|   </table> | ||||
|   <div style="page-break-after: always;"></div> | ||||
| <% end %> | ||||
							
								
								
									
										16
									
								
								app/views/layouts/application.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/views/layouts/application.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>WeddingPlanner</title> | ||||
|     <meta name="viewport" content="width=device-width,initial-scale=1"> | ||||
|     <%= csrf_meta_tags %> | ||||
|     <%= csp_meta_tag %> | ||||
| 
 | ||||
|     <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> | ||||
|     <%= javascript_importmap_tags %> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <%= yield %> | ||||
|   </body> | ||||
| </html> | ||||
| @ -1,5 +1,3 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|  | ||||
| @ -1,3 +1 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <%= yield %> | ||||
|  | ||||
| @ -1,12 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <!doctype html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset='utf-8' /> | ||||
|   </head> | ||||
|     <div id="content"> | ||||
|       <%= yield %> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										9
									
								
								app/views/tables_arrangements/index.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/tables_arrangements/index.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <h1>Tables arrangements</h1> | ||||
| 
 | ||||
| <ol> | ||||
|     <% @tables_arrangements.each_with_index do |tables_arrangement, i| %> | ||||
|         <li> | ||||
|             <p><%= link_to "Arrangement ##{i+1}", tables_arrangement_path(tables_arrangement) %> Discomfort: <%= tables_arrangement.discomfort %></p> | ||||
|         </li> | ||||
|     <% end %> | ||||
| </ol> | ||||
							
								
								
									
										16
									
								
								app/views/tables_arrangements/show.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/views/tables_arrangements/show.html.erb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| <h1>ID: <%= @tables_arrangement.id %></h1> | ||||
| 
 | ||||
| <p>Discomfort: <%= @tables_arrangement.discomfort %></p> | ||||
| 
 | ||||
| <h2>Seats</h2> | ||||
| 
 | ||||
| <% @seats.each do |table_number, seats| %> | ||||
| 
 | ||||
|   <h3>Table <%= table_number %></h3> | ||||
| 
 | ||||
|   <ul> | ||||
|     <% seats.each do |seat| %> | ||||
|       <li><%= seat.guest.full_name %> (<%= seat.guest.affinity_groups.pluck(:name).join(", ") %>)</li> | ||||
|     <% end %> | ||||
|   </ul> | ||||
| <% end %> | ||||
| @ -1,7 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p>Welcome <%= @email %>!</p> | ||||
| 
 | ||||
| <p>You can confirm your account email through the link below:</p> | ||||
| 
 | ||||
| <p><%= link_to 'Confirm my account', confirmation_url(slug: ActsAsTenant.current_tenant&.slug, confirmation_token: @token) %></p> | ||||
| @ -1,9 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p>Hello <%= @email %>!</p> | ||||
| 
 | ||||
| <% if @resource.try(:unconfirmed_email?) %> | ||||
|   <p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p> | ||||
| <% else %> | ||||
|   <p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p> | ||||
| <% end %> | ||||
| @ -1,5 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p>Hello <%= @resource.email %>!</p> | ||||
| 
 | ||||
| <p>We're contacting you to notify you that your password has been changed.</p> | ||||
| @ -1,10 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p>Hello <%= @resource.email %>!</p> | ||||
| 
 | ||||
| <p>Someone has requested a link to change your password. You can do this through the link below.</p> | ||||
| 
 | ||||
| <p><%= link_to 'Change my password', edit_password_url(slug: ActsAsTenant.current_tenant&.slug, reset_password_token: @token) %></p> | ||||
| 
 | ||||
| <p>If you didn't request this, please ignore this email.</p> | ||||
| <p>Your password won't change until you access the link above and create a new one.</p> | ||||
| @ -1,9 +0,0 @@ | ||||
| <%# Copyright (C) 2024-2025 LibreWeddingPlanner contributors %> | ||||
| 
 | ||||
| <p>Hello <%= @resource.email %>!</p> | ||||
| 
 | ||||
| <p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p> | ||||
| 
 | ||||
| <p>Click the link below to unlock your account:</p> | ||||
| 
 | ||||
| <p><%= link_to 'Unlock my account', unlock_url(slug: ActsAsTenant.current_tenant&.slug, unlock_token: @token) %></p> | ||||
							
								
								
									
										7
									
								
								bin/jobs
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								bin/jobs
									
									
									
									
									
								
							| @ -1,7 +0,0 @@ | ||||
| #!/usr/bin/env ruby | ||||
| 
 | ||||
| require_relative "../config/environment" | ||||
| require "solid_queue/cli" | ||||
| 
 | ||||
| SolidQueue.logger = ActiveSupport::Logger.new($stdout) | ||||
| SolidQueue::Cli.start(ARGV) | ||||
| @ -1,8 +1,6 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # This file is used by Rack-based servers to start the application. | ||||
| 
 | ||||
| require_relative 'config/environment' | ||||
| require_relative "config/environment" | ||||
| 
 | ||||
| run Rails.application | ||||
| Rails.application.load_server | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| require_relative 'boot' | ||||
| 
 | ||||
| require 'rails' | ||||
| @ -30,9 +28,6 @@ module WeddingPlanner | ||||
|     # Common ones are `templates`, `generators`, or `middleware`, for example. | ||||
|     config.autoload_lib(ignore: %w[assets tasks]) | ||||
| 
 | ||||
|     # Use a real queuing backend for Active Job (and separate queues per environment). | ||||
|     config.active_job.queue_adapter = :solid_queue | ||||
| 
 | ||||
|     # Configuration for the application, engines, and railties goes here. | ||||
|     # | ||||
|     # These settings can be overridden in specific environments using the files | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) | ||||
| 
 | ||||
| require "bundler/setup" # Set up gems listed in the Gemfile. | ||||
|  | ||||
| @ -83,7 +83,6 @@ test: | ||||
| # | ||||
| production: | ||||
|   <<: *default | ||||
|   host: db | ||||
|   database: wedding_planner_production | ||||
|   username: wedding_planner | ||||
|   password: <%= ENV["WEDDING_PLANNER_DATABASE_PASSWORD"] %> | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| # Load the Rails application. | ||||
| require_relative "application" | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| # Copyright (C) 2024-2025 LibreWeddingPlanner contributors | ||||
| 
 | ||||
| require "active_support/core_ext/integer/time" | ||||
| 
 | ||||
| Rails.application.configure do | ||||
| @ -40,10 +38,8 @@ Rails.application.configure do | ||||
| 
 | ||||
|   # Don't care if the mailer can't send. | ||||
|   config.action_mailer.raise_delivery_errors = false | ||||
| 
 | ||||
|   config.action_mailer.perform_caching = false | ||||
|   config.action_mailer.default_url_options = { host: 'libre-wedding-planner.app.localhost/api' } | ||||
|   config.action_mailer.delivery_method = :letter_opener_web | ||||
|   config.action_mailer.perform_deliveries = true | ||||
| 
 | ||||
|   # Print deprecation notices to the Rails logger. | ||||
|   config.active_support.deprecation = :log | ||||
| @ -77,7 +73,4 @@ Rails.application.configure do | ||||
| 
 | ||||
|   # Raise error when a before_action's only/except options reference missing actions | ||||
|   config.action_controller.raise_on_missing_callback_actions = true | ||||
| 
 | ||||
|   config.hosts << "libre-wedding-planner.app.localhost" | ||||
|   Rails.application.routes.default_url_options[:host] = "libre-wedding-planner.app.localhost" | ||||
| end | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user