mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 03:37:56 +01:00
init activestorage and add avatar upload in user profile
This commit is contained in:
6
Gemfile
6
Gemfile
@@ -70,6 +70,12 @@ gem 'sidekiq-cron', '2.0.1'
|
||||
# Template language
|
||||
gem 'liquid', '5.5.1'
|
||||
|
||||
# S3 for ActiveStorage
|
||||
gem 'aws-sdk-s3', '1.176.1', require: false
|
||||
|
||||
# ActiveStorage validation
|
||||
gem 'active_storage_validations', '1.4'
|
||||
|
||||
group :development, :test do
|
||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
||||
|
||||
|
||||
25
Gemfile.lock
25
Gemfile.lock
@@ -39,6 +39,12 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_storage_validations (1.4.0)
|
||||
activejob (>= 6.1.4)
|
||||
activemodel (>= 6.1.4)
|
||||
activestorage (>= 6.1.4)
|
||||
activesupport (>= 6.1.4)
|
||||
marcel (>= 1.0.3)
|
||||
activejob (6.1.7.9)
|
||||
activesupport (= 6.1.7.9)
|
||||
globalid (>= 0.3.6)
|
||||
@@ -62,6 +68,22 @@ GEM
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1030.0)
|
||||
aws-sdk-core (3.214.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
@@ -125,6 +147,7 @@ GEM
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
jsbundling-rails (1.1.1)
|
||||
railties (>= 6.0.0)
|
||||
json-schema (5.0.1)
|
||||
@@ -319,6 +342,8 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
active_storage_validations (= 1.4)
|
||||
aws-sdk-s3 (= 1.176.1)
|
||||
bootsnap (= 1.12.0)
|
||||
byebug
|
||||
capybara (= 3.40.0)
|
||||
|
||||
@@ -189,7 +189,7 @@ body {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.gravatar {
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.currentUserAvatar {
|
||||
@extend
|
||||
.gravatar,
|
||||
.avatar,
|
||||
.align-self-end,
|
||||
.mr-2;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ ul.usersList {
|
||||
.my-2,
|
||||
.p-3;
|
||||
|
||||
.userGravatar {
|
||||
.userAvatar {
|
||||
@extend .mr-3, .align-self-center;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
.p-2,
|
||||
.my-1;
|
||||
|
||||
.gravatar {
|
||||
.avatar {
|
||||
@extend .align-self-center;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
.mutedText,
|
||||
.mb-2;
|
||||
|
||||
.postAuthorAvatar { @extend .gravatar; }
|
||||
.postAuthorAvatar { @extend .avatar; }
|
||||
}
|
||||
|
||||
.postFooterActions { @extend .d-flex; }
|
||||
|
||||
@@ -39,7 +39,8 @@ class ApplicationController < ActionController::Base
|
||||
:full_name,
|
||||
:notifications_enabled,
|
||||
:recap_notification_frequency,
|
||||
:invitation_token
|
||||
:invitation_token,
|
||||
:avatar,
|
||||
]
|
||||
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: additional_permitted_parameters)
|
||||
|
||||
@@ -11,6 +11,7 @@ class CommentsController < ApplicationController
|
||||
:is_post_update,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
'users.id as user_id', # required for avatar_url
|
||||
'users.full_name as user_full_name',
|
||||
'users.email as user_email',
|
||||
'users.role as user_role',
|
||||
@@ -18,6 +19,12 @@ class CommentsController < ApplicationController
|
||||
.where(post_id: params[:post_id])
|
||||
.left_outer_joins(:user)
|
||||
.order(created_at: :desc)
|
||||
.includes(user: { avatar_attachment: :blob }) # Preload avatars
|
||||
|
||||
comments = comments.map do |comment|
|
||||
user_avatar_url = comment.user.avatar.attached? ? url_for(comment.user.avatar) : nil
|
||||
comment.attributes.merge(user_avatar: user_avatar_url)
|
||||
end
|
||||
|
||||
render json: comments
|
||||
end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Gravatar from 'react-gravatar';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import NewComment from './NewComment';
|
||||
@@ -10,6 +9,7 @@ import { ReplyFormState } from '../../reducers/replyFormReducer';
|
||||
import CommentEditForm from './CommentEditForm';
|
||||
import CommentFooter from './CommentFooter';
|
||||
import { StaffIcon } from '../common/Icons';
|
||||
import Avatar from '../common/Avatar';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
@@ -17,6 +17,7 @@ interface Props {
|
||||
isPostUpdate: boolean;
|
||||
userFullName: string;
|
||||
userEmail: string;
|
||||
userAvatar?: string;
|
||||
userRole: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -70,6 +71,7 @@ class Comment extends React.Component<Props, State> {
|
||||
isPostUpdate,
|
||||
userFullName,
|
||||
userEmail,
|
||||
userAvatar,
|
||||
userRole,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@@ -89,7 +91,7 @@ class Comment extends React.Component<Props, State> {
|
||||
return (
|
||||
<div className="comment">
|
||||
<div className="commentHeader">
|
||||
<Gravatar email={userEmail} size={28} className="gravatar" />
|
||||
<Avatar avatar={userAvatar} email={userEmail} size={28} />
|
||||
<span className="commentAuthor">{userFullName}</span>
|
||||
|
||||
{ userRole > 0 && <StaffIcon /> }
|
||||
|
||||
@@ -41,6 +41,7 @@ const CommentList = ({
|
||||
}: Props) => (
|
||||
<>
|
||||
{comments.map((comment, i) => {
|
||||
console.log('comment', comment);
|
||||
if (comment.parentId === parentId) {
|
||||
return (
|
||||
<div className="commentList" key={i}>
|
||||
|
||||
@@ -25,7 +25,7 @@ const FeedbackListItem = ({ post, onUpdatePostApprovalStatus, hideRejectButton }
|
||||
<div className="feedbackListItemIcon">
|
||||
{
|
||||
post.userId ?
|
||||
<Gravatar email={post.userEmail} size={42} title={post.userEmail} className="gravatar userGravatar" />
|
||||
<Gravatar email={post.userEmail} size={42} title={post.userEmail} className="avatar userAvatar" />
|
||||
:
|
||||
<AnonymousIcon size={42} />
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class UserEditable extends React.Component<Props, State> {
|
||||
editMode === false ?
|
||||
<>
|
||||
<div className="userInfo">
|
||||
<Gravatar email={user.email} size={42} className="gravatar userGravatar" />
|
||||
<Gravatar email={user.email} size={42} className="avatar userAvatar" />
|
||||
|
||||
<div className="userFullNameRoleStatus">
|
||||
<span className="userFullName">{ user.fullName }</span>
|
||||
|
||||
@@ -44,7 +44,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div className="userForm">
|
||||
<Gravatar email={user.email} size={42} className="gravatar userGravatar" />
|
||||
<Gravatar email={user.email} size={42} className="avatar userAvatar" />
|
||||
|
||||
<div className="userFullNameRoleForm">
|
||||
<span className="userFullName">{ user.fullName }</span>
|
||||
|
||||
@@ -42,7 +42,7 @@ const PostUpdateList = ({
|
||||
postUpdates.map((postUpdate, i) => (
|
||||
<div className="postUpdateListItem" key={i}>
|
||||
<div className="postUpdateListItemHeader">
|
||||
<Gravatar email={postUpdate.userEmail} size={28} className="gravatar" />
|
||||
<Gravatar email={postUpdate.userEmail} size={28} className="avatar" />
|
||||
<span>{postUpdate.userFullName}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ const Invitations = ({ siteName, invitations, currentUserEmail, authenticityToke
|
||||
invitationsToDisplay.map((invitation, i) => (
|
||||
<li key={i} className="invitationListItem">
|
||||
<div className="invitationUserInfo">
|
||||
<Gravatar email={invitation.email} size={42} className="gravatar userGravatar" />
|
||||
<Gravatar email={invitation.email} size={42} className="avatar userAvatar" />
|
||||
<span className="invitationEmail">{ invitation.email }</span>
|
||||
</div>
|
||||
|
||||
|
||||
18
app/javascript/components/common/Avatar.tsx
Normal file
18
app/javascript/components/common/Avatar.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import Gravatar from 'react-gravatar';
|
||||
|
||||
interface Props {
|
||||
avatar?: string;
|
||||
email: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Avatar = ({avatar, email, size = 28}: Props) => {
|
||||
if (avatar) {
|
||||
return <img src={avatar} alt={`${email} avatar`} width={size} height={size} className="avatar" />;
|
||||
} else {
|
||||
return <Gravatar email={email} size={size} className="avatar" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
@@ -5,6 +5,7 @@ interface IComment {
|
||||
isPostUpdate: boolean;
|
||||
userFullName: string;
|
||||
userEmail: string;
|
||||
userAvatar?: string;
|
||||
userRole: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ interface ICommentJSON {
|
||||
is_post_update: boolean;
|
||||
user_full_name: string;
|
||||
user_email: string;
|
||||
user_avatar?: string;
|
||||
user_role: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -17,6 +17,7 @@ const initialState: IComment = {
|
||||
isPostUpdate: false,
|
||||
userFullName: '<Unknown user>',
|
||||
userEmail: 'example@example.com',
|
||||
userAvatar: undefined,
|
||||
userRole: 0,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
@@ -36,6 +37,7 @@ const commentReducer = (
|
||||
isPostUpdate: action.comment.is_post_update,
|
||||
userFullName: action.comment.user_full_name,
|
||||
userEmail: action.comment.user_email,
|
||||
userAvatar: action.comment.user_avatar,
|
||||
userRole: action.comment.user_role,
|
||||
createdAt: action.comment.created_at,
|
||||
updatedAt: action.comment.updated_at,
|
||||
|
||||
@@ -11,6 +11,7 @@ class User < ApplicationRecord
|
||||
has_many :likes, dependent: :destroy
|
||||
has_many :comments, dependent: :destroy
|
||||
has_one :api_key, dependent: :destroy
|
||||
has_one_attached :avatar
|
||||
|
||||
enum role: [:user, :moderator, :admin, :owner]
|
||||
enum status: [:active, :blocked, :deleted]
|
||||
@@ -28,6 +29,9 @@ class User < ApplicationRecord
|
||||
format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :password, allow_blank: true, length: { in: 6..128 }
|
||||
validates :password, presence: true, on: :create
|
||||
validates :avatar,
|
||||
content_type: ['image/png', 'image/jpg', 'image/jpeg'],
|
||||
size: { less_than: 100.kilobytes }
|
||||
|
||||
def set_default_role
|
||||
self.role ||= :user
|
||||
|
||||
@@ -27,6 +27,20 @@
|
||||
<%= f.email_field :email, autocomplete: "email", class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :avatar, t('common.forms.auth.avatar') %>
|
||||
|
||||
<p>
|
||||
<% if resource.avatar.attached? %>
|
||||
<%= image_tag url_for(resource.avatar), class: 'avatar', size: 48 %>
|
||||
<% else %>
|
||||
<%= image_tag current_user.gravatar_url, class: 'avatar', size: 48 %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<%= f.file_field :avatar %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div>
|
||||
<b><%= t('common.forms.auth.waiting_confirmation', email: resource.unconfirmed_email) %></b>
|
||||
|
||||
@@ -42,7 +42,12 @@
|
||||
<% if user_signed_in? %>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="profileToggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<%= image_tag(current_user.gravatar_url, class: 'gravatar', alt: current_user.full_name, size: 24) %>
|
||||
<% if current_user.avatar.attached? %>
|
||||
<%= image_tag url_for(current_user.avatar), class: 'avatar', size: 24 %>
|
||||
<% else %>
|
||||
<%= image_tag current_user.gravatar_url, class: 'avatar', alt: current_user.full_name, size: 24 %>
|
||||
<% end %>
|
||||
|
||||
<span class="fullname"><%= current_user.full_name.truncate(24) %></span>
|
||||
<% if current_user.moderator? && Post.pending.length > 0 %>
|
||||
<span class="notificationDot notificationDotTop"><%= Post.pending.length %></span>
|
||||
|
||||
@@ -36,7 +36,7 @@ Rails.application.configure do
|
||||
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = :local
|
||||
config.active_storage.service = :production
|
||||
|
||||
# Mount Action Cable outside main process or domain.
|
||||
# config.action_cable.mount_path = nil
|
||||
|
||||
@@ -13,6 +13,7 @@ en:
|
||||
forms:
|
||||
auth:
|
||||
email: 'Email'
|
||||
avatar: 'Avatar'
|
||||
full_name: 'Full name'
|
||||
password: 'Password'
|
||||
password_confirmation: 'Password confirmation'
|
||||
|
||||
@@ -7,12 +7,17 @@ local:
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
# 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.credentials.dig(:aws, :access_key_id) %>
|
||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||
# region: us-east-1
|
||||
# bucket: your_own_bucket
|
||||
production:
|
||||
service: S3
|
||||
endpoint: storjqualcosa.io
|
||||
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
||||
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||
region: us-east-1
|
||||
bucket: your_own_bucket
|
||||
http_open_timeout: 5
|
||||
http_read_timeout: 10
|
||||
retry_limit: 3
|
||||
|
||||
|
||||
# Remember not to checkin your GCS keyfile to a repository
|
||||
# google:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
# Use Active Record's configured type for primary and foreign keys
|
||||
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
||||
|
||||
create_table :active_storage_blobs, id: primary_key_type do |t|
|
||||
t.string :key, null: false
|
||||
t.string :filename, null: false
|
||||
t.string :content_type
|
||||
t.text :metadata
|
||||
t.string :service_name, null: false
|
||||
t.bigint :byte_size, null: false
|
||||
t.string :checksum, null: false
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [ :key ], unique: true
|
||||
end
|
||||
|
||||
create_table :active_storage_attachments, id: primary_key_type do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
||||
t.references :blob, null: false, type: foreign_key_type
|
||||
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
|
||||
create_table :active_storage_variant_records, id: primary_key_type do |t|
|
||||
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
|
||||
t.string :variation_digest, null: false
|
||||
|
||||
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_and_foreign_key_types
|
||||
config = Rails.configuration.generators
|
||||
setting = config.options[config.orm][:primary_key_type]
|
||||
primary_key_type = setting || :primary_key
|
||||
foreign_key_type = setting || :bigint
|
||||
[primary_key_type, foreign_key_type]
|
||||
end
|
||||
end
|
||||
32
db/schema.rb
32
db/schema.rb
@@ -10,11 +10,39 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2024_11_30_112415) do
|
||||
ActiveRecord::Schema.define(version: 2025_01_03_152157) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.string "filename", null: false
|
||||
t.string "content_type"
|
||||
t.text "metadata"
|
||||
t.string "service_name", null: false
|
||||
t.bigint "byte_size", null: false
|
||||
t.string "checksum", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "api_keys", force: :cascade do |t|
|
||||
t.bigint "tenant_id", null: false
|
||||
t.bigint "user_id", null: false
|
||||
@@ -262,6 +290,8 @@ ActiveRecord::Schema.define(version: 2024_11_30_112415) do
|
||||
t.index ["tenant_id"], name: "index_webhooks_on_tenant_id"
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "api_keys", "tenants"
|
||||
add_foreign_key "api_keys", "users"
|
||||
add_foreign_key "boards", "tenants"
|
||||
|
||||
Reference in New Issue
Block a user