diff --git a/.cloudformation/cloud_front.yml b/.cloudformation/cloud_front.yml deleted file mode 100644 index d85582221f..0000000000 --- a/.cloudformation/cloud_front.yml +++ /dev/null @@ -1,179 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Static contents distribution using CloudFront for elastic Beanstalk. - -Parameters: - AppEnvironment: - Description: Type of app environment. - Type: String - Default: staging - AllowedValues: - - staging - - production - ErrorCacheTTL: - Description: The error cache time in seconds. - Type: String - Default: 1 - EbAlbDnsName: - Description: Type of this AlB domain name like "staging-alb-origin.diycities.jp" - Type: String - DomainAliases: - Description: Type of this CNAME domain name like "staging.diycities.jp,stagingtest.diycities.jp" - Type: CommaDelimitedList - CFSSLCertificateId: - Description: Type of this SSL id - Type: String - WebACLArn: - Description: Type of this AWS WAF arn - Type: String - -Resources: - # ------------------------------------------------------------# - # S3 Bucket - # ------------------------------------------------------------# - CloudFrontLogBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Sub ${AppEnvironment}-cfj-decidim-cloudfront-log - AccessControl: LogDeliveryWrite - LifecycleConfiguration: - Rules: - - Id: !Sub ${AppEnvironment}-cfj-decidim-cloudfront-log-life-cycle - Status: Enabled - Prefix: logs/ - Transitions: - - StorageClass: STANDARD_IA - TransitionInDays: 30 - - S3Bucket: - Type: "AWS::S3::Bucket" - Properties: - BucketName: !Sub ${AppEnvironment}-cfj-decidim - - S3BucketPolicy: - Type: "AWS::S3::BucketPolicy" - Properties: - Bucket: !Sub ${AppEnvironment}-cfj-decidim - PolicyDocument: - Version: "2008-10-17" - Id: "PolicyForCloudFrontPrivateContent" - Statement: - - Sid: "1" - Effect: "Allow" - Principal: - AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}" - Action: "s3:GetObject" - Resource: !Sub "arn:aws:s3:::${AppEnvironment}-cfj-decidim/*" - - - # ------------------------------------------------------------# - # CloudFront - # ------------------------------------------------------------# - StaticCachePolicy: - Type: AWS::CloudFront::CachePolicy - Properties: - CachePolicyConfig: - DefaultTTL: 600 - MinTTL: 60 - MaxTTL: 3600 - Name: !Sub ${AppEnvironment}-decidim-static-cache-policy - ParametersInCacheKeyAndForwardedToOrigin: - CookiesConfig: - CookieBehavior: none - EnableAcceptEncodingBrotli: true - EnableAcceptEncodingGzip: true - HeadersConfig: - HeaderBehavior: none - QueryStringsConfig: - QueryStringBehavior: all - - CloudFront: - Type: AWS::CloudFront::Distribution - Properties: - DistributionConfig: - Aliases: !Ref DomainAliases - Comment: !Sub ${AppEnvironment} cfj-decidim app - Enabled: true - Logging: - IncludeCookies: false - Bucket: !GetAtt CloudFrontLogBucket.DomainName - Prefix: logs/ - Origins: - - Id: appEbOrigin - DomainName: !Ref EbAlbDnsName - CustomOriginConfig: - HTTPPort: 80 - HTTPSPort: 443 - OriginProtocolPolicy: https-only - OriginReadTimeout: 60 - - Id: !Sub "${AppEnvironment}-cfj-decidim.s3.ap-northeast-1.amazonaws.com" - DomainName: !Sub "${AppEnvironment}-cfj-decidim.s3.ap-northeast-1.amazonaws.com" - S3OriginConfig: - OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${OriginAccessIdentity}" - DefaultCacheBehavior: - AllowedMethods: - - GET - - HEAD - - DELETE - - OPTIONS - - PATCH - - POST - - PUT - Compress: false - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad - OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 - TargetOriginId: appEbOrigin - ViewerProtocolPolicy: redirect-to-https - CacheBehaviors: - Quantity: 2 - Items: - - - PathPattern: assets/* - TargetOriginId: appEbOrigin - CachePolicyId: !GetAtt StaticCachePolicy.Id - OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 - ViewerProtocolPolicy: redirect-to-https - AllowedMethods: - - GET - - HEAD - - OPTIONS - Compress: true - - - PathPattern: uploads/* - TargetOriginId: !Sub "${AppEnvironment}-cfj-decidim.s3.ap-northeast-1.amazonaws.com" - ViewerProtocolPolicy: redirect-to-https - AllowedMethods: - - HEAD - - GET - Compress: true - CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 - HttpVersion: http2 - PriceClass: PriceClass_All - ViewerCertificate: - SslSupportMethod: sni-only - MinimumProtocolVersion: TLSv1.2_2019 - AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CFSSLCertificateId}" - CustomErrorResponses: - - ErrorCode: 400 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 403 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 404 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 500 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 502 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 503 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - - ErrorCode: 504 - ErrorCachingMinTTL: !Ref ErrorCacheTTL - WebACLId: !Ref WebACLArn - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-cfj-cloud-front - - OriginAccessIdentity: - Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity" - Properties: - CloudFrontOriginAccessIdentityConfig: - Comment: "画面用s3にアクセスする用" diff --git a/.cloudformation/ecr.yml b/.cloudformation/ecr.yml deleted file mode 100644 index fab277752e..0000000000 --- a/.cloudformation/ecr.yml +++ /dev/null @@ -1,60 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Description: Create decidim-cfj ECR - -Resources: - EcrRepo: - Type: AWS::ECR::Repository - Properties: - RepositoryName: decidim-cfj - ImageScanningConfiguration: - scanOnPush: "true" - ImageTagMutability: "MUTABLE" - LifecyclePolicy: - LifecyclePolicyText: | - { - "rules": [ - { - "rulePriority": 1, - "description": "Delete image without tag after 7 days", - "selection": { - "tagStatus": "untagged", - "countType": "sinceImagePushed", - "countUnit": "days", - "countNumber": 7 - }, - "action": { - "type": "expire" - } - }, - { - "action": { - "type": "expire" - }, - "selection": { - "countType": "imageCountMoreThan", - "countNumber": 7, - "tagStatus": "tagged", - "tagPrefixList": [ - "staging-" - ] - }, - "description": "stagingイメージの削除", - "rulePriority": 2 - }, - { - "action": { - "type": "expire" - }, - "selection": { - "countType": "imageCountMoreThan", - "countNumber": 7, - "tagStatus": "tagged", - "tagPrefixList": [ - "production-" - ] - }, - "description": "productionイメージの削除", - "rulePriority": 3 - } - ] - } diff --git a/.cloudformation/elastic_cache.yml b/.cloudformation/elastic_cache.yml deleted file mode 100644 index 0c7803d45c..0000000000 --- a/.cloudformation/elastic_cache.yml +++ /dev/null @@ -1,39 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Description: "create elastic cache template" - -Parameters: - CacheSubnetGroupName: - Default: decidim-group - Type: String - VpcSecurityGroupId: - Type: CommaDelimitedList - ClusterName: - Type: String - Default: staging-decidim-redis - CacheNodeType: - Type: String - Default: cache.t2.micro - -Metadata: - 'AWS::CloudFormation::Interface': - ParameterGroups: - - Label: - default: 'ElastiCache Parameters' - Parameters: - - CacheNodeType - -Resources: - ElasticCacheRedis: - Type: AWS::ElastiCache::CacheCluster - Properties: - AutoMinorVersionUpgrade: true - AZMode: single-az - CacheNodeType: !Ref CacheNodeType - Engine: redis - EngineVersion: 5.0.6 - NumCacheNodes: 1 - PreferredAvailabilityZone: ap-northeast-1a - PreferredMaintenanceWindow: thu:13:00-thu:14:00 - ClusterName: !Ref ClusterName - CacheSubnetGroupName: !Ref CacheSubnetGroupName - VpcSecurityGroupIds: !Ref VpcSecurityGroupId diff --git a/.cloudformation/vpc_subnets.yml b/.cloudformation/vpc_subnets.yml deleted file mode 100644 index ecc334745d..0000000000 --- a/.cloudformation/vpc_subnets.yml +++ /dev/null @@ -1,116 +0,0 @@ -Parameters: - AppEnvironment: - Description: Type of app environment. - Type: String - Default: staging - AllowedValues: - - staging - - production - -Resources: - PublicVPC: - Type: 'AWS::EC2::VPC' - Properties: - CidrBlock: 172.31.0.0/16 - EnableDnsHostnames: true - EnableDnsSupport: true - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-vpc - PublicSubnet1: - Type: 'AWS::EC2::Subnet' - Properties: - VpcId: !Ref PublicVPC - AvailabilityZone: - Fn::Select: - - 0 - - Fn::GetAZs: "" - CidrBlock: 172.31.0.0/24 - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-subnet1 - PublicSubnet2: - Type: 'AWS::EC2::Subnet' - Properties: - AvailabilityZone: - Fn::Select: - - 1 - - Fn::GetAZs: "" - VpcId: !Ref PublicVPC - CidrBlock: 172.31.1.0/24 - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-subnet2 - PublicSubnet3: - Type: 'AWS::EC2::Subnet' - Properties: - AvailabilityZone: - Fn::Select: - - 2 - - Fn::GetAZs: "" - VpcId: !Ref PublicVPC - CidrBlock: 172.31.2.0/24 - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-subnet3 - InternetGateway: - Type: 'AWS::EC2::InternetGateway' - Properties: - Tags: - - Key: Name - Value: !Sub ${AppEnvironment}-decidim-internet-gateway - GatewayToInternet: - Type: 'AWS::EC2::VPCGatewayAttachment' - Properties: - VpcId: !Ref PublicVPC - InternetGatewayId: !Ref InternetGateway - PublicRouteTable: - Type: 'AWS::EC2::RouteTable' - Properties: - VpcId: !Ref PublicVPC - PublicRoute: - Type: 'AWS::EC2::Route' - DependsOn: GatewayToInternet - Properties: - RouteTableId: !Ref PublicRouteTable - DestinationCidrBlock: 0.0.0.0/0 - GatewayId: !Ref InternetGateway - PublicSubnet1RouteTableAssociation: - Type: 'AWS::EC2::SubnetRouteTableAssociation' - Properties: - SubnetId: !Ref PublicSubnet1 - RouteTableId: !Ref PublicRouteTable - PublicSubnet2RouteTableAssociation: - Type: 'AWS::EC2::SubnetRouteTableAssociation' - Properties: - SubnetId: !Ref PublicSubnet2 - RouteTableId: !Ref PublicRouteTable - PublicSubnet3RouteTableAssociation: - Type: 'AWS::EC2::SubnetRouteTableAssociation' - Properties: - SubnetId: !Ref PublicSubnet3 - RouteTableId: !Ref PublicRouteTable -Outputs: - PublicVPCID: - Description: VPC ID - Value: !Ref "PublicVPC" - Export: - Name: BeanstalkPublicVPCID - PublicSubnet1ID: - Description: Public Subnet A ID - Value: !Ref "PublicSubnet1" - Export: - Name: BeanstalkPublicVPCSubnet1ID - PublicSubnet2ID: - Description: Public Subnet B ID - Value: !Ref "PublicSubnet2" - Export: - Name: BeanstalkPublicVPCSubnet2ID - PublicSubnet3ID: - Description: Public Subnet C ID - Value: !Ref "PublicSubnet3" - Export: - Name: BeanstalkPublicVPCSubnet3ID diff --git a/.cloudformation/waf.yml b/.cloudformation/waf.yml deleted file mode 100644 index 2d5a7fda4d..0000000000 --- a/.cloudformation/waf.yml +++ /dev/null @@ -1,116 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: "Create webAcl" - -Parameters: - AppEnvironment: - Description: Type of app environment. - Type: String - Default: staging - AllowedValues: - - staging - - production - KinesisDeliveryStreamArn: - Description: Type of Kinesis Firehose arn for WAF Log - Type: String - -Resources: -# ------------------------------------------------------------# -# AWS WAFv2 -# ------------------------------------------------------------# - WebAclCloudFront: - Type: AWS::WAFv2::WebACL - Properties: - DefaultAction: - Allow: {} - Description: WebACL for CloudFront - Name: !Sub ${AppEnvironment}-cfj-decidim-web-acl - Rules: - - Name: !Sub ${AppEnvironment}-AWSManagedRulesCommonRuleSet - Priority: 0 - OverrideAction: - None: {} - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: AWSManagedRulesCommonRuleSetMetric - Statement: - ManagedRuleGroupStatement: - ExcludedRules: - - Name: CrossSiteScripting_BODY - - Name: SizeRestrictions_BODY - - Name: GenericRFI_BODY - VendorName: AWS - Name: AWSManagedRulesCommonRuleSet - - Name: !Sub ${AppEnvironment}-AWSManagedRulesKnownBadInputsRuleSet - Priority: 1 - OverrideAction: - None: {} - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: AWSManagedRulesKnownBadInputsRuleSetMetric - Statement: - ManagedRuleGroupStatement: - VendorName: AWS - Name: AWSManagedRulesKnownBadInputsRuleSet - - Name: !Sub ${AppEnvironment}-AWSManagedRulesAmazonIpReputationList - Priority: 2 - OverrideAction: - None: {} - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: AWSManagedRulesAmazonIpReputationListMetric - Statement: - ManagedRuleGroupStatement: - VendorName: AWS - Name: AWSManagedRulesAmazonIpReputationList - - Name: !Sub ${AppEnvironment}-AWSManagedRulesLinuxRuleSet - Priority: 3 - OverrideAction: - None: {} - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: AWSManagedRulesLinuxRuleSetMetric - Statement: - ManagedRuleGroupStatement: - VendorName: AWS - Name: AWSManagedRulesLinuxRuleSet - - Name: !Sub ${AppEnvironment}-AWSManagedRulesSQLiRuleSet - Priority: 4 - OverrideAction: - None: {} - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: AWSManagedRulesSQLiRuleSetMetric - Statement: - ManagedRuleGroupStatement: - ExcludedRules: - - Name: SQLi_BODY - VendorName: AWS - Name: AWSManagedRulesSQLiRuleSet - Scope: CLOUDFRONT - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: !Sub ${AppEnvironment}-cfj-decidim-web-acl - WafLoggingConfig: - Type: AWS::WAFv2::LoggingConfiguration - Properties: - LogDestinationConfigs: - - !Sub ${KinesisDeliveryStreamArn} - RedactedFields: - - SingleHeader: { Name: cookie } - ResourceArn: !Sub "${WebAclCloudFront.Arn}" - LoggingFilter: - DefaultBehavior: DROP - Filters: - - Behavior: KEEP - Conditions: - - ActionCondition: - Action: BLOCK - - ActionCondition: - Action: COUNT - Requirement: MEETS_ANY diff --git a/.cloudformation/waf_kinesis_log.yml b/.cloudformation/waf_kinesis_log.yml deleted file mode 100644 index 4bf94c44bc..0000000000 --- a/.cloudformation/waf_kinesis_log.yml +++ /dev/null @@ -1,111 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Kinesis Firehose for WAF logs - -Parameters: - AppEnvironment: - Description: Type of app environment. - Type: String - Default: staging - AllowedValues: - - staging - - production - -Resources: - S3Bucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Sub ${AppEnvironment}-cfj-decidim-waf-log - AccessControl: LogDeliveryWrite - LifecycleConfiguration: - Rules: - - Id: !Sub ${AppEnvironment}-cfj-decidim-waf-log-life-cycle - Status: Enabled - Transitions: - - StorageClass: STANDARD_IA - TransitionInDays: 30 - - WAFLogDeliveryStream: - Type: AWS::KinesisFirehose::DeliveryStream - Properties: - DeliveryStreamName: !Sub "aws-waf-logs-${AppEnvironment}-cfj-decidim" - DeliveryStreamType: DirectPut - S3DestinationConfiguration: - BucketARN: !Sub "${S3Bucket.Arn}" - BufferingHints: - SizeInMBs: 5 - IntervalInSeconds: 300 - CloudWatchLoggingOptions: - Enabled: true - LogGroupName: !Sub "/aws/kinesisfirehose/aws-waf-logs-${AppEnvironment}-cfj-decidim" - LogStreamName: S3Delivery - CompressionFormat: GZIP - EncryptionConfiguration: - NoEncryptionConfig: NoEncryption - ErrorOutputPrefix: "" - Prefix: "" - RoleARN: !Sub "${FirehoseRole.Arn}" - - WAFLogDeliveryStreamLogGroup: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub "/aws/kinesisfirehose/aws-waf-logs-${AppEnvironment}-cfj-decidim" - - WAFLogDeliveryStreamLogStream: - Type: AWS::Logs::LogStream - Properties: - LogGroupName: !Ref WAFLogDeliveryStreamLogGroup - LogStreamName: S3Delivery - - FirehoseRole: - Type: AWS::IAM::Role - DeletionPolicy: Retain - Properties: - RoleName: !Sub "${AppEnvironment}-cfj-decidim-FirehoseRole" - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: firehose.amazonaws.com - Policies: - - PolicyDocument: - Version: "2012-10-17" - Statement: - - Action: - - glue:GetTable - - glue:GetTableVersion - - glue:GetTableVersions - Effect: Allow - Resource: "*" - - Action: - - s3:AbortMultipartUpload - - s3:GetBucketLocation - - s3:GetObject - - s3:ListBucket - - s3:ListBucketMultipartUploads - - s3:PutObject - Effect: Allow - Resource: - - !Sub "${S3Bucket.Arn}" - - !Sub "${S3Bucket.Arn}/*" - - arn:aws:s3:::%FIREHOSE_BUCKET_NAME% - - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/* - - Action: kms:Decrypt - Effect: Allow - Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/%SSE_KEY_ID%" - - Action: - - lambda:InvokeFunction - - lambda:GetFunctionConfiguration - Effect: Allow - Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%" - - Action: logs:PutLogEvents - Effect: Allow - Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/${S3Bucket}:log-stream:*" - - Action: - - kinesis:DescribeStream - - kinesis:GetShardIterator - - kinesis:GetRecords - Effect: Allow - Resource: !Sub "arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%" - PolicyName: firehose_delivery_role_policy diff --git a/Dockerfile b/Dockerfile index 4ef0355880..6313ec4ff3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ ARG RAILS_ENV="production" ENV LANG=C.UTF-8 \ LC_ALL=C.UTF-8 \ BUNDLER_JOBS=4 \ - BUNDLER_VERSION=1.17.3 \ + BUNDLER_VERSION=2.2.18 \ APP_HOME=/app \ RAILS_ENV=${RAILS_ENV} \ RAILS_LOG_TO_STDOUT=true \ diff --git a/Gemfile.lock b/Gemfile.lock index c68f27ee41..e0f248fa4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -911,4 +911,4 @@ RUBY VERSION ruby 2.7.4p191 BUNDLED WITH - 1.17.3 + 2.2.18 diff --git a/app/commands/decidim/accountability/destroy_all_results.rb b/app/commands/decidim/accountability/destroy_all_results.rb new file mode 100644 index 0000000000..3693c8cf70 --- /dev/null +++ b/app/commands/decidim/accountability/destroy_all_results.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Decidim + module Accountability + # A command with all the business logic when destroys all proposals. + class DestroyAllResults < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all results. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the results and statsues is deleted. + # + # Returns nothing. + def call + Decidim::Accountability::Result.find_each do |result| + if result&.organization == organization + puts "destroy result id: #{result.id}, for component id: #{result.decidim_component_id}" + result.destroy! + end + end + + Decidim::Accountability::Status.find_each do |status| + if status.organization == organization + puts "destroy status id: #{status.id}, for component id: #{status.decidim_component_id}" + status.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/areas/destroy_all_areas.rb b/app/commands/decidim/areas/destroy_all_areas.rb new file mode 100644 index 0000000000..1dddc73871 --- /dev/null +++ b/app/commands/decidim/areas/destroy_all_areas.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Decidim + module Areas + # A command with all the business logic when destroys all areas. + class DestroyAllAreas < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all areas. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the area is deleted. + # + # Returns nothing. + def call + Decidim::Area.find_each do |area| + if area.organization == organization + puts "destroy area id: #{area.id}" + area.destroy! + end + end + + Decidim::AreaType.find_each do |area_type| + if area_type.organization == organization + puts "destroy area_type id: #{area_type.id}" + area_type.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/assemblies/destroy_all_assemblies.rb b/app/commands/decidim/assemblies/destroy_all_assemblies.rb new file mode 100644 index 0000000000..df3f3a5502 --- /dev/null +++ b/app/commands/decidim/assemblies/destroy_all_assemblies.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + # A command with all the business logic when destroys all assemblies. + class DestroyAllAssemblies < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all assemblies. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the assembly is deleted. + # + # Returns nothing. + def call + Decidim::Assembly.where(organization: organization).find_each do |assembly| + puts "destroy assembly id: #{assembly.id}" + assembly.destroy! + end + Decidim::AssembliesType.where(organization: organization).destroy_all + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/blogs/destroy_all_posts.rb b/app/commands/decidim/blogs/destroy_all_posts.rb new file mode 100644 index 0000000000..cdb2f8197c --- /dev/null +++ b/app/commands/decidim/blogs/destroy_all_posts.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Blogs + # A command with all the business logic when destroys all posts. + class DestroyAllPosts < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all posts. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the post is deleted. + # + # Returns nothing. + def call + Decidim::Blogs::Post.find_each do |post| + if post.organization == organization + puts "destroy post id: #{post.id}, for component id: #{post.decidim_component_id}" + post.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/budgets/destroy_all_budgets.rb b/app/commands/decidim/budgets/destroy_all_budgets.rb new file mode 100644 index 0000000000..2d94ffc604 --- /dev/null +++ b/app/commands/decidim/budgets/destroy_all_budgets.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Budgets + # A command with all the business logic when destroys all budgets. + class DestroyAllBudgets < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all budgets. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the budget is deleted. + # + # Returns nothing. + def call + Decidim::Budgets::Budget.find_each do |budget| + if budget.organization == organization + puts "destroy budget id: #{budget.id}, for component id: #{budget.decidim_component_id}" + budget.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/comments/destroy_all_comments.rb b/app/commands/decidim/comments/destroy_all_comments.rb new file mode 100644 index 0000000000..dc41fa5712 --- /dev/null +++ b/app/commands/decidim/comments/destroy_all_comments.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Decidim + module Comments + # A command with all the business logic to destroy a comment + class DestroyAllComments < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all comments. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # + # Returns nothing. + def call + begin + deletable_ids = [] + Decidim::Comments::Comment.find_each(batch_size: 100) do |comment| + if comment&.organization == organization # rubocop:disable Style/IfUnlessModifier + deletable_ids << comment.id + end + rescue Module::DelegationError + # If commentable of comment is nil, the comment should be removed + deletable_ids << comment.id + end + + deletable_ids.reverse.each_slice(30) do |ids| + Decidim::Comments::Comment.where(id: ids).order(id: :desc).each do |comment| + puts "destroy comment id: #{comment.id}, for #{comment.decidim_root_commentable_type}:#{comment.decidim_root_commentable_id}" + # force to delete (ignore validation) + Decidim::Comments::CommentVote.where(comment: comment).delete_all + comment.delete + end + end + rescue Exception => e # rubocop:disable Lint/RescueException + pp "error?: #{e.inspect}" + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/debates/destroy_all_debates.rb b/app/commands/decidim/debates/destroy_all_debates.rb new file mode 100644 index 0000000000..b6fd4a2442 --- /dev/null +++ b/app/commands/decidim/debates/destroy_all_debates.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Debates + # A command with all the business logic when destroys all debates. + class DestroyAllDebates < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all debates. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the debate is deleted. + # + # Returns nothing. + def call + Decidim::Debates::Debate.find_each do |debate| + if debate.organization == organization + puts "destroy debate id: #{debate.id}, for component id: #{debate.decidim_component_id}" + debate.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/destroy_all_attachments.rb b/app/commands/decidim/destroy_all_attachments.rb new file mode 100644 index 0000000000..7d4530b949 --- /dev/null +++ b/app/commands/decidim/destroy_all_attachments.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Decidim + # A command with all the business logic when destroys all proposals. + class DestroyAllAttachments < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all results. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the results and statsues is deleted. + # + # Returns nothing. + def call + Decidim::Attachment.find_each do |attachment| + if attachment.organization == organization + puts "destroy attachment id: #{attachment.id}, for #{attachment.attached_to_type}:#{attachment.attached_to_id}" + attachment.file.purge + attachment.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end +end diff --git a/app/commands/decidim/gamifications/destroy_all_badges.rb b/app/commands/decidim/gamifications/destroy_all_badges.rb new file mode 100644 index 0000000000..71c4c2d811 --- /dev/null +++ b/app/commands/decidim/gamifications/destroy_all_badges.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Decidim + module Gamifications + # A command with all the business logic when destroys all meetings. + class DestroyAllBadges < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all meetings. + def initialize(organization, user) + @organization = organization + @user = user + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the meeting is deleted. + # + # Returns nothing. + def call + if user.organization == organization + Decidim::Gamification::BadgeScore.where(user: user).find_each do |badge_score| + puts "destroy badge_score id: #{badge_score.id}, badge_name: #{badge_score.badge_name}" + badge_score.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization, :user + end + end +end diff --git a/app/commands/decidim/meetings/destroy_all_meetings.rb b/app/commands/decidim/meetings/destroy_all_meetings.rb new file mode 100644 index 0000000000..09c984deac --- /dev/null +++ b/app/commands/decidim/meetings/destroy_all_meetings.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + # A command with all the business logic when destroys all meetings. + class DestroyAllMeetings < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all meetings. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the meeting is deleted. + # + # Returns nothing. + def call + Decidim::Meetings::Meeting.find_each do |meeting| + if meeting.organization == organization + puts "destroy meeting id: #{meeting.id}, for component id: #{meeting.decidim_component_id}" + meeting.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/messaging/destroy_all_messages.rb b/app/commands/decidim/messaging/destroy_all_messages.rb new file mode 100644 index 0000000000..19025d8637 --- /dev/null +++ b/app/commands/decidim/messaging/destroy_all_messages.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Decidim + module Messaging + # A command with all the business logic when destroys all meetings. + class DestroyAllMessages < Rectify::Command + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the message is deleted. + # + # Returns nothing. + def call + Decidim::Messaging::Receipt.find_each do |receipt| + if receipt.recipient.nil? + puts "destroy receipt id: #{receipt.id}" + receipt.destroy! + end + end + + Decidim::Messaging::Message.find_each do |message| + if message.sender.nil? && message.receipts.empty? + puts "destroy message id: #{message.id}" + message.destroy! + end + end + + Decidim::Messaging::Participation.find_each do |participation| + if participation.participant.nil? + puts "destroy participation id: #{participation.id}" + participation.destroy! + end + end + + Decidim::Messaging::Conversation.find_each do |conversation| + if conversation.participations.empty? && conversation.messages.empty? + puts "destroy conversation id: #{conversation.id}" + conversation.destroy! + end + end + + broadcast(:ok) + end + end + end +end diff --git a/app/commands/decidim/organizations/destroy_organization.rb b/app/commands/decidim/organizations/destroy_organization.rb new file mode 100644 index 0000000000..b2210eb871 --- /dev/null +++ b/app/commands/decidim/organizations/destroy_organization.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Decidim + module Organizations + # A command with all the business logic when destroys organization. + class DestroyOrganization < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the organization is deleted. + # + # Returns nothing. + def call + Decidim::DecidimAwesome::AwesomeConfig.find_each do |awesome_config| + if awesome_config.organization == organization + puts "destroy awesome_config id: #{awesome_config.id}" + awesome_config.destroy! + end + end + + Decidim::Verifications::Conflict.find_each do |conflict| + if conflict.current_user.organization == organization || conflict.managed_user.organization == organization + puts "destroy verifications_conflict id: #{conflict.id}" + conflict.destroy! + end + end + + Decidim::DecidimAwesome::EditorImage.where(organization: organization).delete_all + + Decidim::ActionLog.where(organization: organization).delete_all + + Decidim::AssembliesSetting.where(organization: organization).delete_all + + organization.destroy! + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/pages/destroy_all_pages.rb b/app/commands/decidim/pages/destroy_all_pages.rb new file mode 100644 index 0000000000..66b8f42be7 --- /dev/null +++ b/app/commands/decidim/pages/destroy_all_pages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Pages + # A command with all the business logic when destroys all pages. + class DestroyAllPages < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all pages. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the page is deleted. + # + # Returns nothing. + def call + Decidim::Pages::Page.find_each do |page| + if page.organization == organization + puts "destroy page id: #{page.id}, for component id: #{page.decidim_component_id}" + page.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/participatory_processes/destroy_all_participatory_processes.rb b/app/commands/decidim/participatory_processes/destroy_all_participatory_processes.rb new file mode 100644 index 0000000000..8234f19d4f --- /dev/null +++ b/app/commands/decidim/participatory_processes/destroy_all_participatory_processes.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Decidim + module ParticipatoryProcesses + # A command with all the business logic when destroys all participatory_processes. + class DestroyAllParticipatoryProcesses < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all participatory_processes. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the participatory_process is deleted. + # + # Returns nothing. + def call + Decidim::ParticipatoryProcess.where(organization: organization).find_each do |participatory_process| + puts "destroy participatory_process id: #{participatory_process.id}" + participatory_process.destroy! + end + Decidim::ParticipatoryProcessGroup.where(organization: organization).find_each do |participatory_process_group| + puts "destroy participatory_process_group id: #{participatory_process_group.id}" + participatory_process_group.destroy! + end + Decidim::ContentBlock.where(organization: organization).destroy_all + Decidim::Scope.where(organization: organization).destroy_all + Decidim::ScopeType.where(organization: organization).destroy_all + Decidim::StaticPage.where(organization: organization).delete_all ## some static_pages are not removed by `destroy_all` + Decidim::StaticPageTopic.where(organization: organization).destroy_all + Decidim::SearchableResource.where(organization: organization).destroy_all + Decidim::ContextualHelpSection.where(organization: organization).destroy_all + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/proposals/destroy_all_proposals.rb b/app/commands/decidim/proposals/destroy_all_proposals.rb new file mode 100644 index 0000000000..10cea88449 --- /dev/null +++ b/app/commands/decidim/proposals/destroy_all_proposals.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when destroys all proposals. + class DestroyAllProposals < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all proposals. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the proposal is deleted. + # + # Returns nothing. + def call + Decidim::Proposals::Proposal.find_each do |proposal| + if proposal.organization == organization + proposal.amendments.each do |amendment| + puts "destroy amendment id: #{amendment.id}" + amendment.destroy! + end + puts "destroy proposal id: #{proposal.id}, for component id: #{proposal.decidim_component_id}" + proposal.destroy! + end + end + + Decidim::Proposals::CollaborativeDraft.find_each do |draft| + if draft.organization == organization + puts "destroy collaborative draft id: #{draft.id}, for component id: #{draft.decidim_component_id}" + draft.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/commands/decidim/surveys/destroy_all_surveys.rb b/app/commands/decidim/surveys/destroy_all_surveys.rb new file mode 100644 index 0000000000..3576ff7a74 --- /dev/null +++ b/app/commands/decidim/surveys/destroy_all_surveys.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + # A command with all the business logic when destroys all surveys. + class DestroyAllSurveys < Rectify::Command + # Public: Initializes the command. + # + # organization - The organization to destroy all surveys. + def initialize(organization) + @organization = organization + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the survey is deleted. + # + # Returns nothing. + def call + Decidim::Surveys::Survey.find_each do |survey| + if survey.organization == organization + puts "destroy survey id: #{survey.id}, for component id: #{survey.decidim_component_id}" + survey.destroy! + end + end + + broadcast(:ok) + end + + private + + attr_reader :organization + end + end +end diff --git a/app/controllers/concerns/decidim/proposals/cookie_orderable.rb b/app/controllers/concerns/decidim/proposals/cookie_orderable.rb index fb6ea0c42f..59f1356c30 100644 --- a/app/controllers/concerns/decidim/proposals/cookie_orderable.rb +++ b/app/controllers/concerns/decidim/proposals/cookie_orderable.rb @@ -21,7 +21,6 @@ def order_cookie_name def detect_order(candidate) detected = available_orders.detect { |order| order == candidate } cookies[order_cookie_name] = detected if detected - detected end end diff --git a/app/forms/decidim/proposals/admin/proposal_form.rb b/app/forms/decidim/proposals/admin/proposal_form.rb deleted file mode 100644 index 84c7386079..0000000000 --- a/app/forms/decidim/proposals/admin/proposal_form.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Proposals - module Admin - # A form object to be used when admin users want to create a proposal. - class ProposalForm < Decidim::Proposals::Admin::ProposalBaseForm - translatable_attribute :title, String do |field, _locale| - validates field, length: { in: 8..150 }, if: proc { |resource| resource.send(field).present? } - end - translatable_attribute :body, String - - validates :title, :body, translatable_presence: true - - validate :notify_missing_attachment_if_errored - - def map_model(model) - super(model) - presenter = ProposalPresenter.new(model) - - self.title = presenter.title(all_locales: title.is_a?(Hash)) - self.body = presenter.body(all_locales: body.is_a?(Hash)) - self.attachment = if model.documents.first.present? - { file: model.documents.first.file, title: translated_attribute(model.documents.first.title) } - else - {} - end - end - end - end - end -end diff --git a/app/forms/decidim/proposals/proposal_wizard_create_step_form.rb b/app/forms/decidim/proposals/proposal_wizard_create_step_form.rb deleted file mode 100644 index 7b826b79ce..0000000000 --- a/app/forms/decidim/proposals/proposal_wizard_create_step_form.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Proposals - # A form object to be used when public users want to create a proposal. - class ProposalWizardCreateStepForm < Decidim::Form - mimic :proposal - - attribute :title, String - attribute :body, Decidim::Attributes::CleanString - attribute :body_template, String - attribute :user_group_id, Integer - - validates :title, :body, presence: true - validates :title, length: { in: 8..150 } - validates :body, proposal_length: { - minimum: 15, - maximum: ->(record) { record.component.settings.proposal_length } - } - - validate :body_is_not_bare_template - - alias component current_component - - def map_model(model) - self.user_group_id = model.user_groups.first&.id - return unless model.categorization - - self.category_id = model.categorization.decidim_category_id - end - - private - - def body_is_not_bare_template - return if body_template.blank? - - errors.add(:body, :cant_be_equal_to_template) if body.presence == body_template.presence - end - end - end -end diff --git a/app/packs/src/decidim/cfj/html_edit_button.js b/app/packs/src/decidim/cfj/html_edit_button.js new file mode 100644 index 0000000000..1b8a5c5324 --- /dev/null +++ b/app/packs/src/decidim/cfj/html_edit_button.js @@ -0,0 +1,175 @@ +export class HtmlEditButton { + constructor(quill, options) { + let debug = options && options.debug; + console.log("logging enabled"); + // Add button to all quill toolbar instances + const toolbarModule = quill.getModule("toolbar"); + if (!toolbarModule) { + throw new Error( + 'quill.HtmlEditButton requires the "toolbar" module to be included too' + ); + } + this.registerDivModule(); + let toolbarEl = toolbarModule.container; + const buttonContainer = document.createElement("span"); + buttonContainer.setAttribute("class", "ql-formats"); + const button = document.createElement("button"); + button.innerHTML = options.buttonHTML || "<>"; + button.title = options.buttonTitle || "Show HTML source"; + button.onclick = function(e) { + e.preventDefault(); + this.launchPopupEditor(quill, options); + }; + buttonContainer.appendChild(button); + toolbarEl.appendChild(buttonContainer); + } + + registerDivModule() { + // To allow divs to be inserted into html editor + // obtained from issue: https://github.com/quilljs/quill/issues/2040 + var Block = Quill.import("blots/block"); + class Div extends Block {} + Div.tagName = "div"; + Div.blotName = "div"; + Div.allowedChildren = Block.allowedChildren; + Div.allowedChildren.push(Block); + Quill.register(Div); + } + + launchPopupEditor(quill, options) { + const htmlFromEditor = quill.container.querySelector(".ql-editor").innerHTML; + const popupContainer = document.createElement("div"); + const overlayContainer = document.createElement("div"); + const msg = options.msg || 'Edit HTML here, when you click "OK" the quill editor\'s contents will be replaced'; + const cancelText = options.cancelText || "Cancel"; + const okText = options.okText || "Ok"; + + overlayContainer.setAttribute("class", "ql-html-overlayContainer"); + popupContainer.setAttribute("class", "ql-html-popupContainer"); + const popupTitle = document.createElement("i"); + popupTitle.setAttribute("class", "ql-html-popupTitle"); + popupTitle.innerText = msg; + const textContainer = document.createElement("div"); + textContainer.appendChild(popupTitle); + textContainer.setAttribute("class", "ql-html-textContainer"); + const codeBlock = document.createElement("pre"); + codeBlock.setAttribute("data-language", "xml"); + codeBlock.innerText = this.formatHTML(htmlFromEditor); + const htmlEditor = document.createElement("div"); + htmlEditor.setAttribute("class", "ql-html-textArea"); + const buttonCancel = document.createElement("button"); + buttonCancel.innerHTML = cancelText; + buttonCancel.setAttribute("class", "ql-html-buttonCancel"); + const buttonOk = document.createElement("button"); + buttonOk.innerHTML = okText; + const buttonGroup = document.createElement("div"); + buttonGroup.setAttribute("class", "ql-html-buttonGroup"); + + buttonGroup.appendChild(buttonCancel); + buttonGroup.appendChild(buttonOk); + htmlEditor.appendChild(codeBlock); + textContainer.appendChild(htmlEditor); + textContainer.appendChild(buttonGroup); + popupContainer.appendChild(textContainer); + overlayContainer.appendChild(popupContainer); + document.body.appendChild(overlayContainer); + var editor = new Quill(htmlEditor, { + modules: { syntax: options.syntax }, + }); + + buttonCancel.onclick = function() { + document.body.removeChild(overlayContainer); + }; + overlayContainer.onclick = buttonCancel.onclick; + popupContainer.onclick = function(e) { + e.preventDefault(); + e.stopPropagation(); + }; + buttonOk.onclick = function() { + const output = editor.container.querySelector(".ql-editor").innerText; + const noNewlines = output + .replace(/\s+/g, " ") // convert multiple spaces to a single space. This is how HTML treats them + .replace(/(<[^\/<>]+>)\s+/g, "$1") // remove spaces after the start of a new tag + .replace(/<\/(p|ol|ul)>\s/g, "") // remove spaces after the end of lists and paragraphs, they tend to break quill + .replace(/\s<(p|ol|ul)>/g, "<$1>") // remove spaces before the start of lists and paragraphs, they tend to break quill + .replace(/<\/li>\s
  • /g, "
  • ") // remove spaces between list items, they tend to break quill + .replace(/\s<\//g, "]+>)\s(<[^\/<>]+>)/g, "$1$2") // remove space between multiple starting tags + .trim(); + quill.container.querySelector(".ql-editor").innerHTML = noNewlines; + document.body.removeChild(overlayContainer); + }; + } + + // Adapted FROM jsfiddle here: https://jsfiddle.net/buksy/rxucg1gd/ + formatHTML(code) { + // "use strict"; + let stripWhiteSpaces = true; + let stripEmptyLines = true; + const whitespace = " ".repeat(2); // Default indenting 4 whitespaces + let currentIndent = 0; + const newlineChar = "\n"; + let prevChar = null; + let char = null; + let nextChar = null; + + let result = ""; + for (let pos = 0; pos <= code.length; pos++) { + prevChar = char; + char = code.substr(pos, 1); + nextChar = code.substr(pos + 1, 1); + + const isBrTag = code.substr(pos, 4) === "
    "; + const isOpeningTag = char === "<" && nextChar !== "/" && !isBrTag; + const isClosingTag = char === "<" && nextChar === "/" && !isBrTag; + const isTagEnd = prevChar === ">" && char !== "<" && currentIndent > 0; + const isTagNext = !isBrTag && !isOpeningTag && !isClosingTag && isTagEnd && code.substr(pos, code.substr(pos).indexOf("<")).trim() === ""; + if (isBrTag) { + // If opening tag, add newline character and indention + result += newlineChar; + currentIndent--; + pos += 4; + } + if (isOpeningTag) { + // If opening tag, add newline character and indention + result += newlineChar + whitespace.repeat(currentIndent); + currentIndent++; + } + // if Closing tag, add newline and indention + else if (isClosingTag) { + // If there're more closing tags than opening + if (--currentIndent < 0) currentIndent = 0; + result += newlineChar + whitespace.repeat(currentIndent); + } + // remove multiple whitespaces + else if (stripWhiteSpaces === true && char === " " && nextChar === " ") + char = ""; + // remove empty lines + else if (stripEmptyLines === true && char === newlineChar) { + //debugger; + if (code.substr(pos, code.substr(pos).indexOf("<")).trim() === "") + char = ""; + } + if(isTagEnd && !isTagNext) { + result += newlineChar + whitespace.repeat(currentIndent); + } + + result += char; + } + console.dir({ + before: code, + after: result + }); + return result; + } +} + +export const htmlEditButtonOptions = { + debug: true, // logging, default:false + msg: "HTMLソースを編集", //Custom message to display in the editor, default: Edit HTML here, when you click "OK" the quill editor's contents will be replaced + okText: "OK", // Text to display in the OK button, default: Ok, + cancelText: "キャンセル", // Text to display in the cancel button, default: Cancel + buttonHTML: "<>", // Text to display in the toolbar button, default: <> + buttonTitle: "HTMLソースを編集", // Text to display as the tooltip for the toolbar button, default: Show HTML source + syntax: false // Show the HTML with syntax highlighting. Requires highlightjs on window.hljs (similar to Quill itself), default: false +} diff --git a/app/packs/src/decidim/decidim_awesome/editors/editor.js b/app/packs/src/decidim/decidim_awesome/editors/editor.js index 063ad328ac..4620746781 100644 --- a/app/packs/src/decidim/decidim_awesome/editors/editor.js +++ b/app/packs/src/decidim/decidim_awesome/editors/editor.js @@ -18,6 +18,8 @@ import "highlight.js/styles/github.css"; import "src/vendor/image-resize.min" import "src/vendor/image-upload.min" +import { HtmlEditButton, htmlEditButtonOptions } from "src/decidim/cfj/html_edit_button"; + const DecidimAwesome = window.DecidimAwesome || {}; const quillFormats = ["bold", "italic", "link", "underline", "header", "list", "video", "image", "alt", "break", "width", "style", "code", "blockquote", "indent"]; @@ -39,207 +41,6 @@ export function destroyQuillEditor(container) { } export function createQuillEditor(container) { - // decidim-cfj custom start - function $create(elName) { - return document.createElement(elName); - } - function $setAttr(el, key, value) { - return el.setAttribute(key, value); - } - - let debug = false; - const Logger = { - prefixString() { - return ` quill-html-edit-button: `; - }, - get log() { - if (!debug) { - return (...any) => {}; - } - const boundLogFn = console.log.bind(console, this.prefixString()); - return boundLogFn; - } - }; - - class HtmlEditButton { - constructor(quill, options) { - debug = options && options.debug; - Logger.log("logging enabled"); - // Add button to all quill toolbar instances - const toolbarModule = quill.getModule("toolbar"); - if (!toolbarModule) { - throw new Error( - 'quill.HtmlEditButton requires the "toolbar" module to be included too' - ); - } - this.registerDivModule(); - let toolbarEl = toolbarModule.container; - const buttonContainer = $create("span"); - $setAttr(buttonContainer, "class", "ql-formats"); - const button = $create("button"); - button.innerHTML = options.buttonHTML || "<>"; - button.title = options.buttonTitle || "Show HTML source"; - button.onclick = function(e) { - e.preventDefault(); - launchPopupEditor(quill, options); - }; - buttonContainer.appendChild(button); - toolbarEl.appendChild(buttonContainer); - } - - registerDivModule() { - // To allow divs to be inserted into html editor - // obtained from issue: https://github.com/quilljs/quill/issues/2040 - var Block = Quill.import("blots/block"); - class Div extends Block {} - Div.tagName = "div"; - Div.blotName = "div"; - Div.allowedChildren = Block.allowedChildren; - Div.allowedChildren.push(Block); - Quill.register(Div); - } - } - - function launchPopupEditor(quill, options) { - const htmlFromEditor = quill.container.querySelector(".ql-editor").innerHTML; - const popupContainer = $create("div"); - const overlayContainer = $create("div"); - const msg = options.msg || 'Edit HTML here, when you click "OK" the quill editor\'s contents will be replaced'; - const cancelText = options.cancelText || "Cancel"; - const okText = options.okText || "Ok"; - - $setAttr(overlayContainer, "class", "ql-html-overlayContainer"); - $setAttr(popupContainer, "class", "ql-html-popupContainer"); - const popupTitle = $create("i"); - $setAttr(popupTitle, "class", "ql-html-popupTitle"); - popupTitle.innerText = msg; - const textContainer = $create("div"); - textContainer.appendChild(popupTitle); - $setAttr(textContainer, "class", "ql-html-textContainer"); - const codeBlock = $create("pre"); - $setAttr(codeBlock, "data-language", "xml"); - codeBlock.innerText = formatHTML(htmlFromEditor); - const htmlEditor = $create("div"); - $setAttr(htmlEditor, "class", "ql-html-textArea"); - const buttonCancel = $create("button"); - buttonCancel.innerHTML = cancelText; - $setAttr(buttonCancel, "class", "ql-html-buttonCancel"); - const buttonOk = $create("button"); - buttonOk.innerHTML = okText; - const buttonGroup = $create("div"); - $setAttr(buttonGroup, "class", "ql-html-buttonGroup"); - - buttonGroup.appendChild(buttonCancel); - buttonGroup.appendChild(buttonOk); - htmlEditor.appendChild(codeBlock); - textContainer.appendChild(htmlEditor); - textContainer.appendChild(buttonGroup); - popupContainer.appendChild(textContainer); - overlayContainer.appendChild(popupContainer); - document.body.appendChild(overlayContainer); - var editor = new Quill(htmlEditor, { - modules: { syntax: options.syntax }, - }); - - buttonCancel.onclick = function() { - document.body.removeChild(overlayContainer); - }; - overlayContainer.onclick = buttonCancel.onclick; - popupContainer.onclick = function(e) { - e.preventDefault(); - e.stopPropagation(); - }; - buttonOk.onclick = function() { - const output = editor.container.querySelector(".ql-editor").innerText; - const noNewlines = output - .replace(/\s+/g, " ") // convert multiple spaces to a single space. This is how HTML treats them - .replace(/(<[^\/<>]+>)\s+/g, "$1") // remove spaces after the start of a new tag - .replace(/<\/(p|ol|ul)>\s/g, "") // remove spaces after the end of lists and paragraphs, they tend to break quill - .replace(/\s<(p|ol|ul)>/g, "<$1>") // remove spaces before the start of lists and paragraphs, they tend to break quill - .replace(/<\/li>\s
  • /g, "
  • ") // remove spaces between list items, they tend to break quill - .replace(/\s<\//g, "]+>)\s(<[^\/<>]+>)/g, "$1$2") // remove space between multiple starting tags - .trim(); - quill.container.querySelector(".ql-editor").innerHTML = noNewlines; - document.body.removeChild(overlayContainer); - }; - } - - // Adapted FROM jsfiddle here: https://jsfiddle.net/buksy/rxucg1gd/ - function formatHTML(code) { - "use strict"; - let stripWhiteSpaces = true; - let stripEmptyLines = true; - const whitespace = " ".repeat(2); // Default indenting 4 whitespaces - let currentIndent = 0; - const newlineChar = "\n"; - let prevChar = null; - let char = null; - let nextChar = null; - - let result = ""; - for (let pos = 0; pos <= code.length; pos++) { - prevChar = char; - char = code.substr(pos, 1); - nextChar = code.substr(pos + 1, 1); - - const isBrTag = code.substr(pos, 4) === "
    "; - const isOpeningTag = char === "<" && nextChar !== "/" && !isBrTag; - const isClosingTag = char === "<" && nextChar === "/" && !isBrTag; - const isTagEnd = prevChar === ">" && char !== "<" && currentIndent > 0; - const isTagNext = !isBrTag && !isOpeningTag && !isClosingTag && isTagEnd && code.substr(pos, code.substr(pos).indexOf("<")).trim() === ""; - if (isBrTag) { - // If opening tag, add newline character and indention - result += newlineChar; - currentIndent--; - pos += 4; - } - if (isOpeningTag) { - // If opening tag, add newline character and indention - result += newlineChar + whitespace.repeat(currentIndent); - currentIndent++; - } - // if Closing tag, add newline and indention - else if (isClosingTag) { - // If there're more closing tags than opening - if (--currentIndent < 0) currentIndent = 0; - result += newlineChar + whitespace.repeat(currentIndent); - } - // remove multiple whitespaces - else if (stripWhiteSpaces === true && char === " " && nextChar === " ") - char = ""; - // remove empty lines - else if (stripEmptyLines === true && char === newlineChar) { - //debugger; - if (code.substr(pos, code.substr(pos).indexOf("<")).trim() === "") - char = ""; - } - if(isTagEnd && !isTagNext) { - result += newlineChar + whitespace.repeat(currentIndent); - } - - result += char; - } - Logger.log("formatHTML", { - before: code, - after: result - }); - return result; - } - const htmlEditButtonOptions = { - debug: true, // logging, default:false - msg: "HTMLソースを編集", //Custom message to display in the editor, default: Edit HTML here, when you click "OK" the quill editor's contents will be replaced - okText: "OK", // Text to display in the OK button, default: Ok, - cancelText: "キャンセル", // Text to display in the cancel button, default: Cancel - buttonHTML: "<>", // Text to display in the toolbar button, default: <> - buttonTitle: "HTMLソースを編集", // Text to display as the tooltip for the toolbar button, default: Show HTML source - syntax: false // Show the HTML with syntax highlighting. Requires highlightjs on window.hljs (similar to Quill itself), default: false - } - // decidim-cfj custom end - - - - const toolbar = $(container).data("toolbar"); const disabled = $(container).data("disabled"); diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 2d6fd1b4b8..ae7cd7b0e2 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -15,9 +15,8 @@ config.asset_host = ENV.fetch("AWS_CLOUD_FRONT_END_POINT") config.fog_credentials = { provider: "AWS", - aws_access_key_id: Rails.application.secrets.aws_access_key_id, - aws_secret_access_key: Rails.application.secrets.aws_secret_access_key, - region: "ap-northeast-1" + region: "ap-northeast-1", + use_iam_profile: true # host: 's3.ap-northeast-1.amazonaws.com', # endpoint: 'https://s3.example.com:8080' } diff --git a/config/initializers/decidim_override.rb b/config/initializers/decidim_override.rb index aef4d1dab0..60a7e469c4 100644 --- a/config/initializers/decidim_override.rb +++ b/config/initializers/decidim_override.rb @@ -1,9 +1,38 @@ # frozen_string_literal: true -# Override Decidim::Orderable -# -# Use cookies to store default orders -# Rails.application.config.to_prepare do + # Override Decidim::Orderable + # + # Use cookies to store default orders Decidim::Proposals::ProposalsController.prepend Decidim::Proposals::CookieOrderable + + # Decidim::Proposals::ProposalWizardCreateStepForm + # + # minimum title length should be 8 + Decidim::Proposals::ProposalWizardCreateStepForm.validators.each do |validator| + if validator.class == ActiveModel::Validations::LengthValidator && # rubocop:disable Style/Next + validator.attributes.include?(:title) + + fixed_options = validator.options.dup + fixed_options[:minimum] = 8 + validator.instance_eval do + @options = fixed_options.freeze + end + end + end + + # Decidim::Proposals::Admin::ProposalForm + # + # minimum title length should be 8 + Decidim::Proposals::Admin::ProposalForm.validators.each do |validator| + if validator.class == ActiveModel::Validations::LengthValidator && # rubocop:disable Style/Next + validator.attributes.first.match?(/^title_/) + + fixed_options = validator.options.dup + fixed_options[:minimum] = 8 + validator.instance_eval do + @options = fixed_options.freeze + end + end + end end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 62d30788b0..eed5372666 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -72,6 +72,12 @@ ja: does_not_belong: 違法行為、個人情報、または %{organization_name} に属していないと思われる内容が含まれています。 offensive: 差別的な内容、誹謗中傷などの不適切な内容が含まれています。 spam: 本来の内容に関係が無い広告、詐欺や悪意のある処理などが含まれています。 + comment_order_selector: + order: + best_rated: 評価の高い順 + most_discussed: 議論数の多い順 + older: 古い順 + recent: 新しい順 down_vote_button: text: このコメントに同意しません debates: @@ -174,6 +180,27 @@ ja: origin_values: citizens: 一般参加者 official: 事務局 + messaging: + conversation_mailer: + comanagers_new_conversation: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + comanagers_new_message: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + new_conversation: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + new_group_conversation: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + new_group_message: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + new_message: + greeting: "%{recipient}さん、こんにちは。" + outro: '' + moderations: actions: hidden: 非表示 @@ -274,6 +301,15 @@ ja: origin: 起案者 index: collaborative_drafts_list: 共同草案にアクセスする + orders: + label: '提案の並び順:' + most_commented: コメントの多い順 + most_endorsed: オススメの多い順 + most_followed: フォローの多い順 + most_voted: サポートの多い順 + random: ランダム + recent: 新しい順 + with_more_authors: 起案者の多い順 show: link_to_collaborative_draft_help_text: この提案は共同草案の結果です。履歴を確認してください link_to_collaborative_draft_text: 共同草案を見る diff --git a/config/storage.yml b/config/storage.yml index a9774af1bd..721cf25660 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -9,8 +9,6 @@ local: # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) amazon: service: S3 - access_key_id: <%= Rails.application.secrets.aws_access_key_id %> - secret_access_key: <%= Rails.application.secrets.aws_secret_access_key %> region: ap-northeast-1 bucket: <%= ENV.fetch("AWS_BUCKET_NAME", "cfj-decidim") %> diff --git a/db/seeds.rb b/db/seeds.rb index cdf2a7f2e7..f1a59e5c62 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -9,21 +9,6 @@ # Character.create(name: 'Luke', movie: movies.first) # You can remove the 'faker' gem if you don't want Decidim seeds. -require_relative "../lib/monkey_patching_faker" - -# Seeds of DecidimAwesome 0.7.0 and 0.7.2 support Faker 2.x, not 1.9.x. -# So Decidim 0.23.5 (using Faker 1.9.6) should ignore seeds of them. -# -map_component = Decidim.find_component_manifest("awesome_map") -map_component.seeds do |participatory_space| - # noop -end - -iframe_component = Decidim.find_component_manifest("awesome_iframe") -iframe_component.seeds do |participatory_space| - # noop -end - Decidim.seed! if !Rails.env.production? || ENV["SEED"] diff --git a/decidim-comments/lib/decidim/comments/comments_helper.rb b/decidim-comments/lib/decidim/comments/comments_helper.rb index ffe770ae68..b429d6c974 100644 --- a/decidim-comments/lib/decidim/comments/comments_helper.rb +++ b/decidim-comments/lib/decidim/comments/comments_helper.rb @@ -30,7 +30,7 @@ def inline_comments_for(resource, options = {}) machine_translations: machine_translations_toggled?, single_comment: params.fetch("commentId", nil), limit: limit, - order: options[:order] || cookies['comment_default_order'], + order: options[:order] || params['orderable'] || cookies['comment_default_order'], polymorphic: options[:polymorphic] ).to_s end diff --git a/deployments/.ebextensions/01_swap.config b/deployments/.ebextensions/01_swap.config deleted file mode 100644 index 43745ee4bb..0000000000 --- a/deployments/.ebextensions/01_swap.config +++ /dev/null @@ -1,8 +0,0 @@ -commands: - 01setup_swap: - test: test ! -e /swapfile - command: | - /bin/dd if=/dev/zero of=/swapfile bs=1M count=3072 - /bin/chmod 600 /swapfile - /sbin/mkswap /swapfile - /sbin/swapon /swapfile diff --git a/deployments/.ebextensions/02_common_options.config b/deployments/.ebextensions/02_common_options.config deleted file mode 100644 index d3b49ef939..0000000000 --- a/deployments/.ebextensions/02_common_options.config +++ /dev/null @@ -1,25 +0,0 @@ -option_settings: - "aws:autoscaling:launchconfiguration": - IamInstanceProfile: aws-elasticbeanstalk-ec2-role - EC2KeyName: aws-eb - RootVolumeSize: 100 - RootVolumeType: gp2 - "aws:autoscaling:updatepolicy:rollingupdate": - MaxBatchSize: 1 - MinInstancesInService: 1 - RollingUpdateEnabled: true - RollingUpdateType: Health - "aws:autoscaling:trigger": - LowerThreshold: 30 - MeasureName: CPUUtilization - Statistic: Average - Unit: Percent - UpperThreshold: 60 - "aws:elasticbeanstalk:environment": - LoadBalancerType: application - "aws:elasticbeanstalk:environment:proxy": - ProxyServer: none - "aws:elasticbeanstalk:environment:process:default": - MatcherHTTPCode: 301 - "aws:elasticbeanstalk:command": - BatchSize: 30 diff --git a/deployments/.ebextensions/03_nginx.config b/deployments/.ebextensions/03_nginx.config deleted file mode 100644 index 0c5497a760..0000000000 --- a/deployments/.ebextensions/03_nginx.config +++ /dev/null @@ -1,4 +0,0 @@ -container_commands: - 01-healthd-configure: - command: | - chmod -R 777 /var/log/nginx/healthd diff --git a/deployments/.ebignore b/deployments/.ebignore deleted file mode 100644 index 22c6c3d810..0000000000 --- a/deployments/.ebignore +++ /dev/null @@ -1,3 +0,0 @@ -./elasticbeanstalk -/staging -/production diff --git a/deployments/.elasticbeanstalk/config.yml b/deployments/.elasticbeanstalk/config.yml deleted file mode 100644 index f8986a0aa8..0000000000 --- a/deployments/.elasticbeanstalk/config.yml +++ /dev/null @@ -1,14 +0,0 @@ -branch-defaults: - default: - environment: staging -global: - application_name: decidim-app - default_ec2_keyname: null - default_platform: Docker running on 64bit Amazon Linux 2 - default_region: ap-northeast-1 - include_git_submodules: true - instance_profile: null - platform_name: null - platform_version: null - sc: git - workspace_type: Application diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml deleted file mode 100644 index 91f23ad0a0..0000000000 --- a/deployments/docker-compose.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: "3.8" - -services: - nginx-proxy: - image: nginx:1.21 - volumes: - - ./etc/nginx/nginx.conf:/etc/nginx/nginx.conf - - ./etc/nginx/conf.d:/etc/nginx/conf.d - - /var/log/nginx:/var/log/nginx - environment: - TZ: Asia/Tokyo - ports: - - 80:80 - links: - - app - depends_on: - - app - - app: - image: "{RepositoryName}" - environment: - NEW_RELIC_APP_NAME: decidim-app({EBEnvironment}) - env_file: - - .env - - sidekiq: - image: "{RepositoryName}" - command: bundle exec sidekiq -C /app/config/sidekiq.yml - environment: - NEW_RELIC_APP_NAME: decidim-sidekiq({EBEnvironment}) - env_file: - - .env diff --git a/deployments/etc/nginx/conf.d/default.conf b/deployments/etc/nginx/conf.d/default.conf deleted file mode 100644 index 5dc694e14a..0000000000 --- a/deployments/etc/nginx/conf.d/default.conf +++ /dev/null @@ -1,28 +0,0 @@ -server { - listen 80 default_server; - client_max_body_size 20M; - root /usr/share/nginx/html; - - gzip on; - gzip_comp_level 4; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; - if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") { - set $year $1; - set $month $2; - set $day $3; - set $hour $4; - } - - access_log /var/log/nginx/access.log main; - access_log /var/log/nginx/healthd/application.log.$year-$month-$day-$hour healthd; - - location / { - proxy_pass http://app:3000; - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} diff --git a/deployments/etc/nginx/conf.d/timeout.conf b/deployments/etc/nginx/conf.d/timeout.conf deleted file mode 100644 index 1fadc56447..0000000000 --- a/deployments/etc/nginx/conf.d/timeout.conf +++ /dev/null @@ -1,3 +0,0 @@ -proxy_connect_timeout 180; -proxy_send_timeout 180; -proxy_read_timeout 180; diff --git a/deployments/etc/nginx/nginx.conf b/deployments/etc/nginx/nginx.conf deleted file mode 100644 index 11246998be..0000000000 --- a/deployments/etc/nginx/nginx.conf +++ /dev/null @@ -1,32 +0,0 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log; -pid /var/run/nginx.pid; -worker_rlimit_nofile 32137; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - access_log /var/log/nginx/access.log; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - log_format healthd '$msec"$uri"' - '$status"$request_time"$upstream_response_time"' - '$http_x_forwarded_for'; - - include conf.d/*.conf; - - map $http_upgrade $connection_upgrade { - default "upgrade"; - } - - server_tokens off; -} diff --git a/deployments/production-v0-25-2/00_env_options.config b/deployments/production-v0-25-2/00_env_options.config deleted file mode 100644 index 2358fed56a..0000000000 --- a/deployments/production-v0-25-2/00_env_options.config +++ /dev/null @@ -1,21 +0,0 @@ -option_settings: - "aws:elasticbeanstalk:application:environment": - AWS_ACCESS_KEY_ID: "{{resolve:ssm:/decidim-cfj/production/AWS_ACCESS_KEY_ID:1}}" - AWS_SECRET_ACCESS_KEY: "{{resolve:ssm:/decidim-cfj/production/AWS_SECRET_ACCESS_KEY:1}}" - AWS_CLOUD_FRONT_END_POINT: "{{resolve:ssm:/decidim-cfj/production/AWS_CLOUD_FRONT_END_POINT:1}}" - REDIS_URL: "{{resolve:ssm:/decidim-cfj/production/REDIS_URL:5}}" - RDS_DB_NAME: "{{resolve:ssm:/decidim-cfj/production/RDS_DB_NAME:1}}" - RDS_HOSTNAME: "{{resolve:ssm:/decidim-cfj/production/RDS_HOSTNAME:4}}" - RDS_USERNAME: "{{resolve:ssm:/decidim-cfj/production/RDS_USERNAME:1}}" - RDS_PASSWORD: "{{resolve:ssm:/decidim-cfj/production/RDS_PASSWORD:1}}" - SECRET_KEY_BASE: "{{resolve:ssm:/decidim-cfj/production/SECRET_KEY_BASE:1}}" - NEW_RELIC_LICENSE_KEY: "{{resolve:ssm:/decidim-cfj/production/NEW_RELIC_LICENSE_KEY:1}}" - SMTP_ADDRESS: "{{resolve:ssm:/decidim-cfj/production/SMTP_ADDRESS:1}}" - SMTP_USERNAME: "{{resolve:ssm:/decidim-cfj/production/SMTP_USERNAME:2}}" - SMTP_PASSWORD: "{{resolve:ssm:/decidim-cfj/production/SMTP_PASSWORD:2}}" - SMTP_DOMAIN: diycities.jp - AWS_BUCKET_NAME: cfj-decidim-v0252 - RAILS_SKIP_MIGRATIONS: false - DECIDIM_COMMENTS_LIMIT: 30 - SLACK_API_TOKEN: "{{resolve:ssm:/decidim-cfj/production/SLACK_API_TOKEN:1}}" - SLACK_MESSAGE_CHANNEL: #decidim_dev diff --git a/deployments/production-v0-25-2/00_options.config b/deployments/production-v0-25-2/00_options.config deleted file mode 100644 index 13a3c60b7e..0000000000 --- a/deployments/production-v0-25-2/00_options.config +++ /dev/null @@ -1,10 +0,0 @@ -option_settings: - "aws:autoscaling:asg": - MinSize: 3 - MaxSize: 5 - "aws:ec2:instances": - InstanceTypes: t2.large - "aws:elasticbeanstalk:cloudwatch:logs": - StreamLogs: true - DeleteOnTerminate: false - RetentionInDays: 30 diff --git a/deployments/staging/00_env_options.config b/deployments/staging/00_env_options.config deleted file mode 100644 index 4210b6d3d9..0000000000 --- a/deployments/staging/00_env_options.config +++ /dev/null @@ -1,21 +0,0 @@ -option_settings: - "aws:elasticbeanstalk:application:environment": - AWS_ACCESS_KEY_ID: "{{resolve:ssm:/decidim-cfj/staging/AWS_ACCESS_KEY_ID:1}}" - AWS_SECRET_ACCESS_KEY: "{{resolve:ssm:/decidim-cfj/staging/AWS_SECRET_ACCESS_KEY:1}}" - AWS_CLOUD_FRONT_END_POINT: "{{resolve:ssm:/decidim-cfj/staging/AWS_CLOUD_FRONT_END_POINT:1}}" - NEW_RELIC_AGENT_ENABLED: false - REDIS_URL: "{{resolve:ssm:/decidim-cfj/staging/REDIS_URL:1}}" - RDS_DB_NAME: "{{resolve:ssm:/decidim-cfj/staging/RDS_DB_NAME:2}}" - RDS_HOSTNAME: "{{resolve:ssm:/decidim-cfj/staging/RDS_HOSTNAME:1}}" - RDS_USERNAME: "{{resolve:ssm:/decidim-cfj/staging/RDS_USERNAME:1}}" - RDS_PASSWORD: "{{resolve:ssm:/decidim-cfj/staging/RDS_PASSWORD:1}}" - SECRET_KEY_BASE: "{{resolve:ssm:/decidim-cfj/staging/SECRET_KEY_BASE:1}}" - SMTP_ADDRESS: "{{resolve:ssm:/decidim-cfj/production/SMTP_ADDRESS:1}}" - SMTP_USERNAME: "{{resolve:ssm:/decidim-cfj/production/SMTP_USERNAME:2}}" - SMTP_PASSWORD: "{{resolve:ssm:/decidim-cfj/production/SMTP_PASSWORD:2}}" - SMTP_DOMAIN: diycities.jp - AWS_BUCKET_NAME: staging-cfj-decidim - RAILS_SKIP_MIGRATIONS: false - DECIDIM_COMMENTS_LIMIT: 30 - SLACK_API_TOKEN: "{{resolve:ssm:/decidim-cfj/staging/SLACK_API_TOKEN:1}}" - SLACK_MESSAGE_CHANNEL: #decidim_dev diff --git a/deployments/staging/00_options.config b/deployments/staging/00_options.config deleted file mode 100644 index 735998362e..0000000000 --- a/deployments/staging/00_options.config +++ /dev/null @@ -1,19 +0,0 @@ -option_settings: - "aws:autoscaling:asg": - MinSize: 1 - MaxSize: 2 - "aws:ec2:instances": - EnableSpot: true - InstanceTypes: t2.small - SpotFleetOnDemandBase: 0 - SpotMaxPrice: 0.1 - "aws:elasticbeanstalk:cloudwatch:logs": - StreamLogs: true - DeleteOnTerminate: true - RetentionInDays: 14 - "aws:elasticbeanstalk:managedactions": - ManagedActionsEnabled: true - PreferredStartTime: "Sun:18:00" - "aws:elasticbeanstalk:managedactions:platformupdate": - UpdateLevel: minor - InstanceRefreshEnabled: true diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f0473d8e95..f5b6be12fe 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,39 +1,25 @@ -# ※GUIでElastic Beanstalkの設定をいじらないでください。デプロイで戻ります。 - -Elastic Beanstalkの設定はコードで管理されています。 +aws環境の設定はAWS CDKのコードで管理されています。 インスタンスタイプやオートスケールの設定が違うため、stagingとproductionで一部ファイルが別です。それ以外の共通の設定は同じファイルを使っているので気を付けて下さい。 -共通: [deployments/.ebextensions](/deployments/.ebextensions) - -staging: [deployments/staging](/deployments/staging) -production: [deployments/production](/deployments/production) +staging: [config/staging](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main/config/staging.json) +production: [config/production](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main/config/prd-v0252.json) デプロイの際に上記の設定ファイルを元にデプロイが実行されます。コードでの設定がある場合、インフラも含め反映されます。 急ぎで、GUIで変更することはあると思います。しかし、GUIだけ変更してソースコードを変更しないと、デプロイの際に戻って事故の原因になります。なので、ソースコードに反映してください。 -GUIの設定とコードの書き方は、[公式](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/command-options-general.html#command-options-general-elasticbeanstalkapplicationenvironment) を参考にしてください。 - -環境変数は環境別に設定する値だけ、[deployments/production/00_env_options.config](/deployments/production/00_env_options.config) or [deployments/staging/00_env_options.config](/deployments/staging/00_env_options.config) に記載して下さい。 - 秘密鍵などのSSM経由で参照される値は、デプロイ時に動的に展開されます。 -``` -{{resolve:ssm:ssmのパラメータの名前:バージョン}} -``` - -https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/dynamic-references.html - # GitHubからデプロイ ワークフローの設定:[.github/workflows/deploy.yml](/.github/workflows/deploy.yml) -デプロイの基本設定: [deployments/](/deployments/) +デプロイの基本設定: [decidim-cfj-cdk](https://github.com/codeforjapan/decidim-cfj-cdk) 1. ECRにログイン -1. Dockerコンテナをbuild -1. short commit hashを含む環境ごとのタグで、ECRにDockerイメージをpush -1. elastic beanstalkに該当のイメージを指定してデプロイ +2. Dockerコンテナをbuild +3. short commit hashを含む環境ごとのタグで、ECRにDockerイメージをpush +4. ECSに該当のイメージを指定してデプロイ Dockerイメージのタグ例: staging-dfasfste @@ -69,90 +55,140 @@ developブランチにpushすると自動でデプロイされます。 普通にデプロイするのと同様に戻したい先commitに対してタグを打ちます。 バグの場合、バグが発生したcommitの1つ前のcommitにたいしてタグを打ちます。 -コンテナイメージはbuild済みなので、すぐにeb deployが実行されます。 +コンテナイメージはbuild済みなので、すぐにcdk deployが実行されます。 ### staging revetしてdevelopブランチにpushしてください。 -# CfJ Decidim AWS への Install(Beanstalk 編) +# CfJ Decidim AWS への Install(cdk 編) + +# 検証環境構築手順 + +# 0. decidim-cfj-cdkのリポジトリの準備 +手元に [decidim-cfj-cdk](https://github.com/codeforjapan/decidim-cfj-cdk)のリポジトリをクローンし、以下の作業は全てそのディレクトリ以下で行う + +# 1. AWS (Amazon Web Services) へのアクセスする準備 + +## 1-1. config/dev.json の作成 + +[dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main/config/dev.json) ファイルを作成し、[config](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config) ディレクトリ下に配置。 -## 1. Install AWS tools and setup user +## 1-2. credentials fileの作成 -[こちらの手順書](https://platoniq.github.io/decidim-install/decidim-aws/) の手順2を実施 +[設定ファイルと認証情報ファイルの設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html)を参考に、credentialsファイルを作成する。 -## 2. Get Decidim source code -```bash -git clone https://github.com/codeforjapan/decidim-cfj.git +今回は、`~/.aws/credentials`に以下のようprofileが`decidim`になるよう作成。(違う名前にした場合、以下読み変えが必要です。) ``` +[decidim] +aws_access_key_id=YOUR_ACCESS_KEY +aws_secret_access_key=YOUR_SECRET_ACCESS_KEY +``` + +# 2. SESの設定 +[Amazon Simple Email Service を設定する](https://docs.aws.amazon.com/ja_jp/ses/latest/dg/setting-up.html)や、[Setup email](https://docs.aws.amazon.com/ja_jp/ses/latest/dg/setting-up.html)を参考に、AWS SESの準備を行う。 + +# 2-1. [dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json) を編集する +[dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json)の’smtpDomain’を用意したドメインに書き換える。 + -## 3. Bundle install +# 3. パラメータストアに環境変数を登録する -Ruby 環境はインストール済とする(ruby 2.7.4p191) +# 3-1 シークレットの作成 +AWS Systems Manager のパラメータストアで以下のようなパラメータを手動で作成する。 -```bash -cd decidim-cfj -bundle install +``` + /decidim-cfj/${props.stage}/AWS_ACCESS_KEY_ID + /decidim-cfj/${props.stage}/AWS_SECRET_ACCESS_KEY + /decidim-cfj/${props.stage}/AWS_CLOUD_FRONT_END_POINT + /decidim-cfj/${props.stage}/RDS_DB_NAME + /decidim-cfj/${props.stage}/RDS_USERNAME + /decidim-cfj/${props.stage}/RDS_PASSWORD + /decidim-cfj/${props.stage}/SECRET_KEY_BASE + /decidim-cfj/${props.stage}/NEW_RELIC_LICENSE_KEY + /decidim-cfj/${props.stage}/SMTP_ADDRESS + /decidim-cfj/${props.stage}/SMTP_USERNAME ``` -## 4. Elastic Beanstalk に 環境をセットアップする +# 4. ECRの準備 -1. [docker-compose.yml](/deployments/docker-compose.yml)で`{RepositoryName}`をデプロイしたいECRのイメージパスに修正。 -1. [docker-compose.yml](/deployments/docker-compose.yml)で`{EBEnvironment}`をデプロイする環境名に修正。 -1. 作成したい環境の設定をコピー。 +# 4-1 プライベートリポジトリを作成する +[プライベートリポジトリを作成する](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html) を参考に AWS ECRのリポジトリを用意する。 -[deployments/.elasticbeanstalk/config.yml](/deployments/.elasticbeanstalk/config.yml)に設定があるので、基本的に何も聞かれないはずです。 +# 4-2 用意したリポジトリにdecidim の docker imageをpushする +手元の環境で、[decidim-cfj](https://github.com/codeforjapan/decidim-cfj)のdocker imageをbuildし、 +[Docker イメージをプッシュする](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html)を参考に +buildしたdocker imageを用意したリポジトリにpushする。 +# 4-3 [dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json) を編集する +[dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json)の’repository’部分に用意したECRリポジトリ名、’tag’部分にpushした際のtagに書き換える。 -```bash -cd deployments +# 5. 証明書の準備 -# production -cp production/*.config .ebextensions/ -# staging(台数とかログの保持期間が小さい) -cp staging/*.config .ebextensions/ +# 5-1 任意のドメインをroute53に用意し、aws certificate managerで証明書を発行する +[証明書を発行して管理する](https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/gs.html)を参考に証明書を発行、Arnをメモする -eb create production --process -``` +# 5-2 [dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json) を編集する +[dev.json](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main//config/dev.json)の’certificates’部分にメモしたArnに書き換える。 + +# 6. デプロイ -最後エラーで終わるがOK +## 6-1. cdk bootstrap の実行 -## 4. Secret Key Base を設定 +使用するリージョンごとに一回実行する必要がある。2回目以降は不要。 -```bash -eb setenv SECRET_KEY_BASE=$(bin/rails secret) +```console +$ npx cdk --context stage=dev --profile decidim bootstrap ``` -## 5. Postgres データベースを作成する +## 6-2. デプロイ前の差分確認 -必要な設定を行い、DBを立ち上げる +どんなリソースが作成されるのかを確認できる。 -その後`eb deploy`を実行 +```console +$ npx cdk --context stage=dev --profile decidim diff +``` + +## 6-3. デプロイ実行 + +```console +$ npx cdk --context stage=dev --profile decidim deploy --all --require-approval never +``` -## 6. CNAME 設定とSSL設定 +上記コマンドが成功すれば、デプロイは成功です。 -Elastic Beanstalk のインスタンスをAレコードとして割り当てる +## 6-4. デプロイ確認 -ロードバランサの設定をする(手順[6.2 Configure SSL](https://platoniq.github.io/decidim-install/decidim-aws/#62-configure-ssl) ) +`devdecidimStack` とCloudFormationのスタック一覧から[検索](https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks?filteringStatus=active&filteringText=devdecidimStack&viewNested=true&hideStacks=false&stackId=)し、以下のように各Stackに`CREATE_COMPLETE`が表示されていることを確認してください。 -## 7. 最初のユーザを作る +# 7. 初回デプロイ -[6.3 Create the first admin user](https://platoniq.github.io/decidim-install/decidim-aws/#63-create-the-first-admin-user) -に従う(root で) +## 7.1 環境へのアクセス +```console +$ aws ecs execute-command --region ap-northeast-1 --cluster devDecidimCluster --task ${タスク名} --container appContainer --interactive --command "/bin/ash" --profile decidim +``` -## 8. SES の設定をする +## 7.2 migrateとseedの実行 +```console +$ ./bin/rails db:migrate +$ ./bin/rails db:seed SEED=true +``` -[6.4 Setup email](https://platoniq.github.io/decidim-install/decidim-aws/#64-setup-email) +## 7.3 環境へのアクセス +``dev-decidim-alb-origin.${指定したドメイン}``で管理画面にアクセス -## 9. Redis の設定をする +### 8. 別のドメインを追加する場合 -[6.5 Configure the job system with Sidekiq and Redis](https://platoniq.github.io/decidim-install/decidim-aws/#65-configure-the-job-system-with-sidekiq-and-redis) +## cloudfrontの代替ドメイン名に対象のドメインを追加 +cloudfrontの管理画面に行き作成したcloudfrontの管理画面で代替ドメインに追加したいドメインを加えて保存する -sidekiqの設定をする必要はありません。dockerでデプロイされています。 +## decidimの管理画面にアクセスし、対象の組織にドメインを設定する +設定したい組織にドメインを設定して、対象のドメインいアクセスする -stagingはcloud formationで作成しています。 [./INFRA.md#Redis](./INFRA.md#Redis) +手順を変えるとエラーでアクセスに失敗します。 -## 10. S3 の設定をする +### seedの実行について +decidimではproduction環境のseedは以下のenvをつけて実行する必要があります +SEED=true bundle exec rake db:seed -[6.6 File storage](https://platoniq.github.io/decidim-install/decidim-aws/#66-file-storage) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 058edd136f..0901b30468 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -9,7 +9,7 @@ Dockerで環境を構築する際は、1.環境構築と2. 実行(ローカル | アプリケーション名 | バージョン | |-------------------------------------------|--------| | [Ruby](https://www.ruby-lang.org/ja/) | 2.7.4 | -| [Bundler](https://bundler.io/) | 1.17.3 | +| [Bundler](https://bundler.io/) | 2.2.18 | | [PostgreSQL](https://www.postgresql.org/) | 13 | ### 1-1. 事前準備 @@ -35,7 +35,7 @@ git checkout -b master origin/master ``` ### 2.4 bundlerのインストール ``` -gem install bundler:1.17.3 +gem install bundler:2.2.18 ``` ### 2.5 DBのユーザーとパスワードの設定 @@ -56,25 +56,15 @@ export DATABASE_DBNAME_DEV= ``` bundle install ``` -### 2.7 言語の設定 -```bash -# default_localeを`:en`にセットする -vim config/initializers/decidim.rb - -# before -config.default_locale = :ja -# after -config.default_locale = :en -``` -### 2.8 DB作成からシードまで +### 2.7 DB作成からシードまで ``` bin/rails db:create db:migrate bin/rails db:seed ``` -### 2.9 サーバー起動 +### 2.8 サーバー起動 bin/rails s -### 2.10 お疲れさまでした +### 2.9 お疲れさまでした http://localhost:3000 にアクセス ## 3. 実行(Dockerバージョン) @@ -97,27 +87,17 @@ git checkout -b master origin/master docker-compose build ``` -### 3.4 言語の設定 -``` -# default_localeを`:en`にセットする -vim config/initializers/decidim.rb -# before -config.default_locale = :ja -# after -config.default_locale = :en -``` - -### 3.5 DB作成からシードまで +### 3.4 DB作成からシードまで ``` docker-compose run --rm app ./bin/rails db:create db:migrate docker-compose run --rm app ./bin/rails db:seed ``` -### 3.6 サーバー起動 +### 3.5 サーバー起動 ``` docker-compose up -d ``` -### 3.7 お疲れさまでした +### 3.6 お疲れさまでした http://localhost:3000 にアクセス ## 4. テスト用アカウント情報 diff --git a/docs/INFRA.md b/docs/INFRA.md index 39246427c6..bc3191606c 100644 --- a/docs/INFRA.md +++ b/docs/INFRA.md @@ -1,4 +1,4 @@ -# Cloud Formation の使い方 +# CDK の使い方 AWS の code as a infrastructureサービスCloud Formationで、インフラストラクチャを構築しています。 @@ -6,92 +6,10 @@ AWS の code as a infrastructureサービスCloud Formationで、インフラス これにより、本番とstagingの環境を簡単に揃えられます。 -テンプレートファイルは、[.cloudformation](/.cloudformation)にまとめます。1テンプレートに多くのリソースを含むと、変更が大きくなるので分割して作成してください。 +CDKは [decidim-cfj-cdk](https://github.com/codeforjapan/decidim-cfj-cdk)にまとめています。 -# 実行順 +# 実行方法 +こちらのドキュメントをご覧ください +[検証環境構築手順](https://github.com/codeforjapan/decidim-cfj-cdk/blob/main/docs/build_dev.md) -Cloud Front とWAFには依存関係があるので、下記の順で作成することを勧めます。 - -1. Kinesis firehose For WAF log -1. WAF Web ACL -1. Cloud Front - -## VPC & Subnets - -下記を参考に作成しています。VPC 1つ、public Subnet 3です。 -https://github.com/awsdocs/elastic-beanstalk-samples/blob/main/cfn-templates/vpc-public.yaml - -### Template file - -[.cloudformation/vpc_subnets.yml](/.cloudformation/vpc_subnets.yml) - -ログ出力用のs3バケットも作成してます。 - -### Stack Name - -- staging-decidim-app-cloud-front -- production-decidim-app-cloud-front - -## Redis - -シンプルに本体を作成するだけです。サブネットグループ等は現状手動作成となります。 - -### Template file - -[.cloudformation/elastic_cache.yml](/.cloudformation/elastic_cache.yml) - -### Stack Name - -- staging-decidim-redis - -## Cloud Front - -キャッシュポリシーとクラウドフロント本体を作成します。 - -### Template file - -[.cloudformation/cloud_front.yml](/.cloudformation/cloud_front.yml) - -### Stack Name - -- staging-decidim-app-cloud-front -- production-decidim-app-cloud-front - -## WAF - -Cloud frontに合わせてus-east-1にあります。 - -### Template file - -[.cloudformation/waf.yml](/.cloudformation/waf.yml) - -### Stack Name - -- staging-decidim-waf -- production-decidim-waf - -## WAF - -Cloud frontに合わせてus-east-1にあります。 - -### Template file - -[.cloudformation/waf_kinesis_log.yml](/.cloudformation/waf_kinesis_log.yml) - -### Stack Name - -- staging-decidim-kinesis-waf-log -- production-decidim-kinesis-waf-log - -## ECR - -staging用と本番は同じリポジトリです。Dockerイメージのタグで区別します。 - -### Template file - -[.cloudformation/ecr.yml](/.cloudformation/ecr.yml) - -### Stack Name - -- decidim-cfj-ecr-repository diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 5aad66dc5e..a684586b03 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -19,19 +19,27 @@ Decidim本体のバージョンを更新する際、特に注意が必要な内 QuillエディタでHTML編集ができるようにするために追加されたファイル。現在はDecidim Awesome対応になっています(decidim_awesome内の`app/packs/src/decidim/decidim_awesome/editors/editor.js`がベースになっています)。 -* `app/packs/stylesheets/buttons.scss` +* `app/packs/stylesheets/decidim/cfj/buttons.scss` `https://github.com/codeforjapan/decidim-cfj/issues/46` の対応で `https://github.com/codeforjapan/decidim-cfj/pull/96` で追加しています。 -* `app/packs/stylesheets/comment_content.scss` +* `app/packs/stylesheets/decidim/cfj/comment_content.scss` https://github.com/codeforjapan/decidim-cfj/pull/337 で追加されたファイル。コメント本文の改行をCSSで制御するためのものです。 -* `app/packs/stylesheets/forms.scss` +* `app/packs/stylesheets/decidim/cfj/forms.scss` https://github.com/codeforjapan/decidim-cfj/pull/94 で追加されたファイル。職業欄の見た目を修正するためのもの。 -* `app/packs/stylesheets/search.scss` +* `app/packs/stylesheets/decidim/cfj/media_print.scss` + + https://github.com/codeforjapan/decidim-cfj/pull/460 で追加されたファイル。印刷用のCSSファイル。 + +* `app/packs/stylesheets/decidim/cfj/ql_html_editor.scss` + + https://github.com/codeforjapan/decidim-cfj/pull/469 で追加されたファイル。Quill HTML Editor用のCSSファイル。 + +* `app/packs/stylesheets/decidim/cfj/search.scss` https://github.com/codeforjapan/decidim-cfj/pull/348 で追加されたファイル。グローバル検索が日本語では機能していないため削除したもの。 @@ -52,11 +60,6 @@ Decidim本体のバージョンを更新する際、特に注意が必要な内 https://github.com/codeforjapan/decidim-cfj/pull/415 で追加されたファイル。ディベートでconclusionsに空文字列を許すための修正。 -* `app/forms/decidim/proposals/proposal_wizard_create_step_form.rb`, `app/forms/decidim/proposals/admin/proposal_form.rb` - - https://github.com/codeforjapan/decidim-cfj/issues/23 の対応のために追加されたもの。対応するPRは https://github.com/codeforjapan/decidim-cfj/pull/60 https://github.com/codeforjapan/decidim-cfj/pull/163 です。 - EtiquetteValidatorは修正が入っているので戻せるかもしれませんが、8文字程度のタイトルでも許可するようにする修正はフィードバックできていません。 - * `decidim-comments` https://github.com/codeforjapan/decidim-cfj/issues/319 などの対応のために追加されたディレクトリ(gem)。 @@ -97,3 +100,8 @@ Decidim本体のバージョンを更新する際、特に注意が必要な内 * `decidim-user_extension/app/views/decidim/account/show.html.erb`, `decidim-user_extension/app/views/decidim/account/_user_extension.html.erb` `decidim-core/app/views/decidim/account/show.html.erb` を上書きしています。 + +* `lib/decidim/map/provider/static_map`以下 + +`Decidim::Map::Provider::StaticMap::CfjOsm`という独自のstatic map providerを定義するためのものです。 +`config/initializers/decidim.rb`のconfig.maps以下のstaticのところで導入されています。 diff --git a/lib/monkey_patching_faker.rb b/lib/monkey_patching_faker.rb deleted file mode 100644 index 91e7c7fbf6..0000000000 --- a/lib/monkey_patching_faker.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -# This monkey patch force the output of `Faker::Internet.slug()` to be in English. -module FakerInternetEnglishExtension - def slug(words: nil, glue: nil) - super - with_locale(:en) do - glue ||= sample(%w(- _)) - (words || Faker::Lorem.words(number: 2).join(" ")).delete(",.").gsub(" ", glue).downcase - end - end -end - -Faker::Internet.singleton_class.prepend(FakerInternetEnglishExtension) diff --git a/lib/tasks/delete.rake b/lib/tasks/delete.rake new file mode 100644 index 0000000000..6639c51413 --- /dev/null +++ b/lib/tasks/delete.rake @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +namespace :delete do + desc "Destroy all components for a given organization" + task destroy_all: [ + :destroy_all_comments, + :destroy_all_attachments, + :destroy_all_accountability, + :destroy_all_budgets, + :destroy_all_proposals, + :destroy_all_blogs, + :destroy_all_debates, + :destroy_all_meetings, + :destroy_all_pages, + :destroy_all_surveys, + :destroy_all_users, + :destroy_all_assemblies, + :destroy_all_participatory_processes, + :destroy_all_areas, + :destroy_all_newsletters, + :destroy_organization, + :destroy_all_messages + ] + + desc "Destroy all comments for a given organization" + task destroy_all_comments: :environment do + puts "Start destroy_all_comments of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Comments::Comment.transaction do + Decidim::Comments::DestroyAllComments.call(organization) + end + + puts "Finish destroy_all_comments of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all accountability for a given organization" + task destroy_all_accountability: :environment do + puts "Start destroy_all_accountability of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Accountability::Result.transaction do + Decidim::Accountability::DestroyAllResults.call(organization) + end + + puts "Finish destroy_all_accountability of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all attachments for a given organization" + task destroy_all_attachments: :environment do + puts "Start destroy_all_attachments of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Attachment.transaction do + Decidim::DestroyAllAttachments.call(organization) + end + + puts "Finish destroy_all_attachments of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all budgets for a given organization" + task destroy_all_budgets: :environment do + puts "Start destroy_all_budgets of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Budgets::Budget.transaction do + Decidim::Budgets::DestroyAllBudgets.call(organization) + end + + puts "Finish destroy_all_budgets of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all proposals for a given organization" + task destroy_all_proposals: :environment do + puts "Start destroy_all_proposals of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Proposals::Proposal.transaction do + Decidim::Proposals::DestroyAllProposals.call(organization) + end + + puts "Finish destroy_all_proposals of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all blogs for a given organization" + task destroy_all_blogs: :environment do + puts "Start destroy_all_blogs of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Blogs::Post.transaction do + Decidim::Blogs::DestroyAllPosts.call(organization) + end + + puts "Finish destroy_all_blogs of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all debates for a given organization" + task destroy_all_debates: :environment do + puts "Start destroy_all_debates of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Debates::Debate.transaction do + Decidim::Debates::DestroyAllDebates.call(organization) + end + + puts "Finish destroy_all_debates of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all meetings for a given organization" + task destroy_all_meetings: :environment do + puts "Start destroy_all_meetings of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Meetings::Meeting.transaction do + Decidim::Meetings::DestroyAllMeetings.call(organization) + end + + puts "Finish destroy_all_meetings of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all pages for a given organization" + task destroy_all_pages: :environment do + puts "Start destroy_all_pages of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Pages::Page.transaction do + Decidim::Pages::DestroyAllPages.call(organization) + end + + puts "Finish destroy_all_pages of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all surveys for a given organization" + task destroy_all_surveys: :environment do + puts "Start destroy_all_surveys of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Surveys::Survey.transaction do + Decidim::Surveys::DestroyAllSurveys.call(organization) + end + + puts "Finish destroy_all_surveys of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all assemblies for a given organization" + task destroy_all_assemblies: :environment do + puts "Start destroy_all_assemblies of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Assembly.transaction do + Decidim::Assemblies::DestroyAllAssemblies.call(organization) + end + + puts "Finish destroy_all_assemblies of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all participatory_processes for a given organization" + task destroy_all_participatory_processes: :environment do + puts "Start destroy_all_participatory_processes of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::ParticipatoryProcess.transaction do + Decidim::ParticipatoryProcesses::DestroyAllParticipatoryProcesses.call(organization) + end + + puts "Finish destroy_all_participatory_processes of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all areas for a given organization" + task destroy_all_areas: :environment do + puts "Start destroy_all_areas of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Area.transaction do + Decidim::Areas::DestroyAllAreas.call(organization) + end + + puts "Finish destroy_all_areas of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all newsletters for a given organization" + task destroy_all_newsletters: :environment do + puts "Start destroy_all_newsletters of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Newsletter.transaction do + Decidim::Newsletter.where(organization: organization).destroy_all + end + + puts "Finish destroy_all_newsletters of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all users for a given organization" + task destroy_all_users: :environment do + puts "Start destroy_all_users of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + form = OpenStruct.new(valid?: true, delete_reason: "Testing") + Decidim::User.transaction do + Decidim::User.where(organization: organization).find_each(batch_size: 100) do |user| + Decidim::Gamifications::DestroyAllBadges.call(organization, user) + Decidim::Authorization.where(user: user).destroy_all + end + end + + # Use tranzaction in Decidim::DestroyAccount + Decidim::User.where(organization: organization).find_each(batch_size: 100) do |user| + puts "destroy user id: #{user.id}" + Decidim::DestroyAccount.call(user, form) + rescue StandardError => e + puts "Decidim::DestroyAccount failed: #{e.inspect}" + end + + puts "Finish destroy_all_users of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy a given organization" + task destroy_organization: :environment do + puts "Start destroy_organization of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + + organization = decidim_find_organization + return unless organization + + Decidim::Organization.transaction do + Decidim::Organizations::DestroyOrganization.call(organization) + end + + puts "Finish destroy_organization of #{ENV["DECIDIM_ORGANIZATION_NAME"]}" + end + + desc "Destroy all messages for a given organization" + task destroy_all_messages: :environment do + puts "Start destroy_all_messages" + + Decidim::Messaging::Message.transaction do + Decidim::Messaging::DestroyAllMessages.call + end + + puts "Finish destroy_all_messages" + end +end + +private + +def decidim_find_organization + organization = Decidim::Organization.find_by(name: ENV["DECIDIM_ORGANIZATION_NAME"]) + + unless organization + puts "Organization not found: '#{ENV["DECIDIM_ORGANIZATION_NAME"]}'" + puts "Usage: DECIDIM_ORGANIZATION_NAME= rails delete::destroy_all" + return + end + + puts "Organization found: '#{ENV["DECIDIM_ORGANIZATION_NAME"]}' as '#{organization.id}'" + + organization +end diff --git a/spec/factories.rb b/spec/factories.rb index 072e68cc9a..d9527ea57c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -3,6 +3,7 @@ require "decidim/core/test/factories" require "decidim/cfj/test/factories" require "decidim/debates/test/factories" +require "decidim/proposals/test/factories" FactoryBot.define do sequence(:valid_jwt) do |_n| diff --git a/spec/forms/decidim/proposals/admin/admin_proposal_form_spec.rb b/spec/forms/decidim/proposals/admin/admin_proposal_form_spec.rb new file mode 100644 index 0000000000..a4479e6523 --- /dev/null +++ b/spec/forms/decidim/proposals/admin/admin_proposal_form_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "rails_helper" + +require_relative "../../../../shared/proposal_form_examples" + +module Decidim + module Proposals + module Admin + describe ProposalForm do + describe "shared examples in official Decidim repository" do + before { Rails.application.config.i18n.default_locale = Decidim.default_locale = :en } + + after { Rails.application.config.i18n.default_locale = Decidim.default_locale = :ja } + + it_behaves_like "a proposal form", skip_etiquette_validation: true, i18n: true, address_optional_with_geocoding: true + it_behaves_like "a proposal form with meeting as author", skip_etiquette_validation: true, i18n: true + end + + describe "minimum title length" do + subject { form } + + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization: organization) } + let(:component) { create(:proposal_component, participatory_space: participatory_space) } + let(:title) { { ja: "提案のテスト・1" } } + let(:body) { { ja: "提案のテストその1です。タイトルの文字数をテストします。" } } + let(:created_in_meeting) { true } + let(:meeting_component) { create(:meeting_component, participatory_space: participatory_space) } + let(:author) { create(:meeting, component: meeting_component) } + let!(:meeting_as_author) { author } + + let(:params) do + { + title: title, + body: body, + created_in_meeting: created_in_meeting, + author: meeting_as_author, + meeting_id: author.id + } + end + + let(:form) do + described_class.from_params(params).with_context( + current_component: component, + current_organization: component.organization, + current_participatory_space: participatory_space + ) + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when title is too short" do + let(:title) { { ja: "提案のテスト1" } } + + it { is_expected.to be_invalid } + + it "only adds errors to this field" do + subject.valid? + expect(subject.errors.keys).to eq [:title_ja] + end + end + end + end + end + end +end diff --git a/spec/forms/decidim/proposals/proposal_wizard_create_step_form_spec.rb b/spec/forms/decidim/proposals/proposal_wizard_create_step_form_spec.rb new file mode 100644 index 0000000000..019aeae397 --- /dev/null +++ b/spec/forms/decidim/proposals/proposal_wizard_create_step_form_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require "rails_helper" + +module Decidim + module Proposals + describe ProposalWizardCreateStepForm do + subject { form } + + let(:params) do + { + title: title, + body: body, + body_template: body_template, + user_group_id: user_group.id + } + end + + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization: organization) } + let(:component) { create(:proposal_component, participatory_space: participatory_space) } + let(:title) { "More sidewalks and less roads" } + let(:body) { "Cities need more people, not more cars" } + let(:body_template) { nil } + let(:author) { create(:user, organization: organization) } + let(:user_group) { create(:user_group, :verified, users: [author], organization: organization) } + + let(:form) do + described_class.from_params(params).with_context( + current_component: component, + current_organization: component.organization, + current_participatory_space: participatory_space + ) + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when there's no title" do + let(:title) { nil } + + it { is_expected.to be_invalid } + + it "only adds errors to this field" do + subject.valid? + expect(subject.errors.keys).to eq [:title] + end + end + + context "when the title is not long enough" do + let(:component) { create(:proposal_component, participatory_space: participatory_space) } + let(:title) { "A short title" } + + context "with short title settings" do + it { is_expected.to be_valid } + end + end + + context "when there's no body" do + let(:body) { nil } + + it { is_expected.to be_invalid } + end + + context "when the body exceeds the permited length" do + let(:component) { create(:proposal_component, :with_proposal_length, participatory_space: participatory_space, proposal_length: allowed_length) } + let(:allowed_length) { 15 } + let(:body) { "A body longer than the permitted" } + + it { is_expected.to be_invalid } + + context "with carriage return characters that cause it to exceed" do + let(:allowed_length) { 80 } + let(:body) { "This text is just the correct length\r\nwith the carriage return characters removed" } + + it { is_expected.to be_valid } + end + end + + context "when there's a body template set" do + let(:body_template) { "This is the template" } + + it { is_expected.to be_valid } + + context "when the template and the body are the same" do + let(:body) { body_template } + + it { is_expected.to be_invalid } + end + end + end + end +end diff --git a/spec/shared/proposal_form_examples.rb b/spec/shared/proposal_form_examples.rb new file mode 100644 index 0000000000..609d18cd3b --- /dev/null +++ b/spec/shared/proposal_form_examples.rb @@ -0,0 +1,428 @@ +# frozen_string_literal: true + +shared_examples "a proposal form" do |options| + subject { form } + + let(:organization) { create(:organization, available_locales: [:en]) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization: organization) } + let(:component) { create(:proposal_component, participatory_space: participatory_space) } + let(:title) do + if options[:i18n] == false + "More sidewalks and less roads!" + else + { en: "More sidewalks and less roads!" } + end + end + let(:body) do + if options[:i18n] == false + "Everything would be better" + else + { en: "Everything would be better" } + end + end + let(:author) { create(:user, organization: organization) } + let(:user_group) { create(:user_group, :verified, users: [author], organization: organization) } + let(:user_group_id) { user_group.id } + let(:category) { create(:category, participatory_space: participatory_space) } + let(:parent_scope) { create(:scope, organization: organization) } + let(:scope) { create(:subscope, parent: parent_scope) } + let(:category_id) { category.try(:id) } + let(:scope_id) { scope.try(:id) } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:has_address) { false } + let(:address) { nil } + let(:suggested_hashtags) { [] } + let(:attachment_params) { nil } + let(:meeting_as_author) { false } + let(:params) do + { + title: title, + body: body, + author: author, + category_id: category_id, + scope_id: scope_id, + address: address, + has_address: has_address, + meeting_as_author: meeting_as_author, + attachment: attachment_params, + suggested_hashtags: suggested_hashtags + } + end + + let(:form) do + described_class.from_params(params).with_context( + current_component: component, + current_organization: component.organization, + current_participatory_space: participatory_space + ) + end + + describe "scope" do + let(:current_component) { component } + + it_behaves_like "a scopable resource" + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when there's no title" do + let(:title) { nil } + + it { is_expected.to be_invalid } + + it "only adds errors to this field" do + subject.valid? + if options[:i18n] + expect(subject.errors.keys).to eq [:title_en] + else + expect(subject.errors.keys).to eq [:title] + end + end + end + + context "when the title is too long" do + let(:title) do + if options[:i18n] == false + "A" * 200 + else + { en: "A" * 200 } + end + end + + it { is_expected.to be_invalid } + end + + context "when the title is the minimum length" do + let(:title) do + if options[:i18n] == false + "Length is right" + else + { en: "Length is right" } + end + end + + it { is_expected.to be_valid } + end + + unless options[:skip_etiquette_validation] + context "when the body is not etiquette-compliant" do + let(:body) do + if options[:i18n] == false + "A" + else + { en: "A" } + end + end + + it { is_expected.to be_invalid } + end + end + + context "when there's no body" do + let(:body) { nil } + + it { is_expected.to be_invalid } + end + + context "when no category_id" do + let(:category_id) { nil } + + it { is_expected.to be_valid } + end + + context "when no scope_id" do + let(:scope_id) { nil } + + it { is_expected.to be_valid } + end + + context "with invalid category_id" do + let(:category_id) { 987 } + + it { is_expected.to be_invalid } + end + + context "when geocoding is enabled" do + let(:component) { create(:proposal_component, :with_geocoding_enabled, participatory_space: participatory_space) } + + context "when the has address checkbox is checked" do + let(:has_address) { true } + + context "when the address is not present" do + it "does not store the coordinates" do + expect(subject).to be_valid + expect(subject.address).to be(nil) + expect(subject.latitude).to be(nil) + expect(subject.longitude).to be(nil) + end + end + + context "when the address is present" do + let(:address) { "Some address" } + + before do + stub_geocoding(address, [latitude, longitude]) + end + + it "validates the address and store its coordinates" do + expect(subject).to be_valid + expect(subject.latitude).to eq(latitude) + expect(subject.longitude).to eq(longitude) + end + end + end + + context "when latitude and longitude are manually set" do + context "when the has address checkbox is unchecked" do + let(:has_address) { false } + + it "is valid" do + expect(subject).to be_valid + expect(subject.latitude).to eq(nil) + expect(subject.longitude).to eq(nil) + end + end + + context "when the proposal is unchanged" do + let(:previous_proposal) { create(:proposal, address: address) } + + let(:title) do + if options[:skip_etiquette_validation] + previous_proposal.title + else + translated(previous_proposal.title) + end + end + + let(:body) do + if options[:skip_etiquette_validation] + previous_proposal.body + else + translated(previous_proposal.body) + end + end + + let(:params) do + { + id: previous_proposal.id, + title: title, + body: body, + author: previous_proposal.authors.first, + category_id: previous_proposal.try(:category_id), + scope_id: previous_proposal.try(:scope_id), + has_address: has_address, + address: address, + attachment: previous_proposal.try(:attachment_params), + latitude: latitude, + longitude: longitude + } + end + + it "is valid" do + expect(subject).to be_valid + expect(subject.latitude).to eq(latitude) + expect(subject.longitude).to eq(longitude) + end + end + end + end + + describe "category" do + subject { form.category } + + context "when the category exists" do + it { is_expected.to be_kind_of(Decidim::Category) } + end + + context "when the category does not exist" do + let(:category_id) { 7654 } + + it { is_expected.to eq(nil) } + end + + context "when the category is from another process" do + let(:category_id) { create(:category).id } + + it { is_expected.to eq(nil) } + end + end + + it "properly maps category id from model" do + proposal = create(:proposal, component: component, category: category) + + expect(described_class.from_model(proposal).category_id).to eq(category_id) + end + + if options && options[:user_group_check] + it "properly maps user group id from model" do + proposal = create(:proposal, component: component, users: [author], user_groups: [user_group]) + + expect(described_class.from_model(proposal).user_group_id).to eq(user_group_id) + end + end + + context "when the attachment is present" do + let(:params) do + { + title: title, + body: body, + author: author, + category_id: category_id, + scope_id: scope_id, + address: address, + has_address: has_address, + meeting_as_author: meeting_as_author, + suggested_hashtags: suggested_hashtags, + add_photos: [Decidim::Dev.test_file("city.jpeg", "image/jpeg")] + } + end + + it { is_expected.to be_valid } + + context "when the form has some errors" do + let(:title) { nil } + + it "adds an error to the `:attachment` field" do + expect(subject).not_to be_valid + + if options[:i18n] + expect(subject.errors.full_messages).to match_array(["Title en can't be blank", "Add photos Needs to be reattached"]) + expect(subject.errors.keys).to match_array([:title_en, :add_photos]) + else + expect(subject.errors.full_messages).to match_array(["Title can't be blank", "Title is too short (under 15 characters)", "Add photos Needs to be reattached"]) + expect(subject.errors.keys).to match_array([:title, :add_photos]) + end + end + end + end + + describe "#extra_hashtags" do + subject { form.extra_hashtags } + + let(:component) do + create( + :proposal_component, + :with_extra_hashtags, + participatory_space: participatory_space, + suggested_hashtags: component_suggested_hashtags, + automatic_hashtags: component_automatic_hashtags + ) + end + let(:component_automatic_hashtags) { "" } + let(:component_suggested_hashtags) { "" } + + it { is_expected.to eq([]) } + + context "when there are auto hashtags" do + let(:component_automatic_hashtags) { "HashtagAuto1 HashtagAuto2" } + + it { is_expected.to eq(%w(HashtagAuto1 HashtagAuto2)) } + end + + context "when there are some suggested hashtags checked" do + let(:component_suggested_hashtags) { "HashtagSuggested1 HashtagSuggested2 HashtagSuggested3" } + let(:suggested_hashtags) { %w(HashtagSuggested1 HashtagSuggested2) } + + it { is_expected.to eq(%w(HashtagSuggested1 HashtagSuggested2)) } + end + + context "when there are invalid suggested hashtags checked" do + let(:component_suggested_hashtags) { "HashtagSuggested1 HashtagSuggested2" } + let(:suggested_hashtags) { %w(HashtagSuggested1 HashtagSuggested3) } + + it { is_expected.to eq(%w(HashtagSuggested1)) } + end + + context "when there are both suggested and auto hashtags" do + let(:component_automatic_hashtags) { "HashtagAuto1 HashtagAuto2" } + let(:component_suggested_hashtags) { "HashtagSuggested1 HashtagSuggested2" } + let(:suggested_hashtags) { %w(HashtagSuggested2) } + + it { is_expected.to eq(%w(HashtagAuto1 HashtagAuto2 HashtagSuggested2)) } + end + end +end + +shared_examples "a proposal form with meeting as author" do |options| + subject { form } + + let(:organization) { create(:organization, available_locales: [:en]) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization: organization) } + let(:component) { create(:proposal_component, participatory_space: participatory_space) } + let(:title) { { en: "More sidewalks and less roads!" } } + let(:body) { { en: "Everything would be better" } } + let(:created_in_meeting) { true } + let(:meeting_component) { create(:meeting_component, participatory_space: participatory_space) } + let(:author) { create(:meeting, component: meeting_component) } + let!(:meeting_as_author) { author } + + let(:params) do + { + title: title, + body: body, + created_in_meeting: created_in_meeting, + author: meeting_as_author, + meeting_id: author.id + } + end + + let(:form) do + described_class.from_params(params).with_context( + current_component: component, + current_organization: component.organization, + current_participatory_space: participatory_space + ) + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when there's no title" do + let(:title) { nil } + + it { is_expected.to be_invalid } + end + + context "when the title is too long" do + let(:title) do + if options[:i18n] == false + "A" * 200 + else + { en: "A" * 200 } + end + end + + it { is_expected.to be_invalid } + end + + unless options[:skip_etiquette_validation] + context "when the body is not etiquette-compliant" do + let(:body) do + if options[:i18n] == false + "A" + else + { en: "A" } + end + end + + it { is_expected.to be_invalid } + end + end + + context "when there's no body" do + let(:body) { nil } + + it { is_expected.to be_invalid } + end + + context "when proposals comes from a meeting" do + it "validates the meeting as author" do + expect(subject).to be_valid + expect(subject.author).to eq(author) + end + end +end