Compare commits

..

4 commits

Author SHA1 Message Date
Devin Howard
98081097b4 Revert "update rails dependencies"
Original commit broke the build, since some dependencies have breaking
changes.

This reverts commit f753392d49.
2018-11-09 07:42:26 -08:00
Devin Howard
f753392d49 update rails dependencies 2018-10-17 19:51:51 -07:00
Connor Turland
38a209a970 small bug fix ()
if someone besides one of "us" tried to change their password, and their settings, it wouldn't work

in the typical case it would work fine
2018-03-11 21:57:23 -07:00
Devin Howard
bd7bf20810
factory_girl => factory_bot () 2018-03-10 08:10:09 -08:00
59 changed files with 225 additions and 964 deletions

View file

@ -38,7 +38,7 @@ gem 'uglifier'
group :test do
gem 'brakeman', require: false
gem 'factory_girl_rails'
gem 'factory_bot_rails'
gem 'json-schema'
gem 'rspec-rails'
gem 'shoulda-matchers'

View file

@ -105,10 +105,10 @@ GEM
actionmailer (>= 4.0, < 6)
activesupport (>= 4.0, < 6)
execjs (2.7.0)
factory_girl (4.8.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_girl_rails (4.8.0)
factory_girl (~> 4.8.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
faker (1.8.4)
i18n (~> 0.5)
@ -311,7 +311,7 @@ DEPENDENCIES
doorkeeper
dotenv-rails
exception_notification
factory_girl_rails
factory_bot_rails
faker
httparty
jquery-rails

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="document" viewBox="0 0 8 8">
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3zm-3 2h1v1h-1v-1zm0 2h1v1h-1v-1zm0 2h4v1h-4v-1z" />
</svg>

Before

(image error) Size: 218 B

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="file" viewBox="0 0 8 8">
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3z" />
</svg>

Before

(image error) Size: 168 B

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="image" viewBox="0 0 8 8">
<path d="M0 0v8h8v-8h-8zm1 1h6v3l-1-1-1 1 2 2v1h-1l-4-4-1 1v-3z" />
</svg>

Before

(image error) Size: 188 B

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="musical-note" viewBox="0 0 8 8">
<path d="M8 0c-5 0-6 1-6 1v4.093999999999999c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-3.969c.732-.226 1.99-.438 4-.5v2.063c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-5.5z"
/>
</svg>

Before

(image error) Size: 394 B

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="8" data-icon="question-mark" data-container-transform="translate(2)" viewBox="0 0 8 8">
<path d="M4.469 0c-.854 0-1.48.256-1.875.656s-.54.901-.594 1.281l1 .125c.036-.26.125-.497.313-.688.188-.19.491-.375 1.156-.375.664 0 1.019.163 1.219.344.199.181.281.405.281.656 0 .833-.313 1.063-.813 1.5-.5.438-1.188 1.083-1.188 2.25v.25h1v-.25c0-.833.344-1.063.844-1.5.5-.438 1.156-1.083 1.156-2.25 0-.479-.168-1.02-.594-1.406-.426-.387-1.071-.594-1.906-.594zm-.5 7v1h1v-1h-1z"
/>
</svg>

Before

(image error) Size: 552 B

Binary file not shown.

Before

(image error) Size: 47 KiB

Binary file not shown.

Before

(image error) Size: 1.1 KiB

Binary file not shown.

Before

(image error) Size: 1.4 KiB

Binary file not shown.

Before

(image error) Size: 2.6 KiB

Binary file not shown.

Before

(image error) Size: 1.8 KiB

View file

@ -13,13 +13,6 @@ Metamaps.ServerData['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.
Metamaps.ServerData['exploremaps_sprite.png'] = '<%= asset_path 'exploremaps_sprite.png' %>'
Metamaps.ServerData['map_control_sprite.png'] = '<%= asset_path 'map_control_sprite.png' %>'
Metamaps.ServerData['user_sprite.png'] = '<%= asset_path 'user_sprite.png' %>'
Metamaps.ServerData.attachmentFileTypeIcons = {
pdf: '<%= asset_path('attachmentFileTypeIcons//open-iconic-document.svg') %>',
text: '<%= asset_path('attachmentFileTypeIcons//open-iconic-file.svg') %>',
image: '<%= asset_path('attachmentFileTypeIcons//open-iconic-image.svg') %>',
audio: '<%= asset_path('attachmentFileTypeIcons//open-iconic-musical-note.svg') %>',
unknown: '<%= asset_path('attachmentFileTypeIcons//open-iconic-question-mark.svg') %>'
}
Metamaps.ServerData.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %>
Metamaps.ServerData.REALTIME_SERVER = '<%= ENV['REALTIME_SERVER'] %>'
Metamaps.ServerData.RAILS_ENV = '<%= ENV['RAILS_ENV'] %>'

View file

@ -197,7 +197,6 @@ $mid-gray-opacity: rgba(66, 66, 66, 0.6);
.CardOnGraph .links {
position: relative;
background-color: #e0e0e0;
z-index: 2;
.linkItem {
@ -626,6 +625,156 @@ background-color: #E0E0E0;
z-index:100;
}
#embedlyLinkLoader {
margin: 0 auto;
width: 28px;
}
.CardOnGraph .link-adder {
width:100%;
height:47px;
position: relative;
}
.link-adder a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin-left: 40px;
padding-top:9px;
font-size: 16px;
line-height: 16px;
}
#addlink, #addupload {
display: inline-block;
width: 102px;
height: 12px;
text-align: left;
padding: 18px 0 18px 48px;
font-size: 12px;
color: #9e9e9e;
cursor: pointer;
position: relative;
}
#addlink:hover, #addupload:hover {
color: #616161;
}
.attachmentIcon {
background-repeat: no-repeat;
background-position: 0 0;
width: 24px;
height: 24px;
position: absolute;
top: 12px;
left: 12px;
}
#linkIcon {
background-image: url(<%= asset_data_uri('link_sprite.png') %>);
}
#uploadIcon {
background-image: url(<%= asset_data_uri('upload_sprite.png') %>);
}
#addlink:hover #linkIcon, #addupload:hover #uploadIcon {
background-position: 0 -24px;
}
.addLink {
position: relative;
}
#addLinkInput {
height: 32px;
width: 268px;
padding: 8px 16px 8px 16px;
position: relative;
border: none;
line-height: 14px;
}
#addLinkInput input{
padding: 9px 27px 9px 31px;
height: 12px;
width: 210px;
margin: 0 0 0 0;
border: none;
outline: none;
font-size: 12px;
line-height: 12px;
background: white;
color: black;
font-family: 'din-regular', helvetica, sans-serif;
}
#addLinkIcon {
position: absolute;
top: 12px;
left: 20px;
width: 24px;
height: 24px;
background-repeat: no-repeat;
background-position: 0 0;
background-image: url(<%= asset_data_uri('link_sprite.png') %>);
pointer-events: none;
z-index: 1;
}
#addLinkReset {
position: absolute;
top: 8px;
right: 15px;
width: 32px;
height: 32px;
cursor: pointer;
float:none;
background-image: url(<%= asset_data_uri('remove.png') %>);
background-repeat: no-repeat;
background-position: center center;
}
.embeds.nonEmbedlyLink {
padding-top: 24px;
}
#embedlyLink {
border-left: 8px solid #CCC;
overflow: hidden;
padding: 8px;
padding-left: 12px;
-moz-box-shadow: 1px 1px 5px 0 #ccc;
-webkit-box-shadow: 1px 1px 5px 0 #ccc;
box-shadow: 1px 1px 5px 0 #ccc;
-moz-border-radius-topright: 5px;
-webkit-border-top-right-radius: 5px;
border-top-right-radius: 5px;
-moz-border-radius-bottomright: 8px;
-webkit-border-bottom-right-radius: 8px;
border-bottom-right-radius: 8px;
margin: 8px;
}
.linkActions {
position: relative;
}
.CardOnGraph .embeds {
position: relative;
overflow: hidden;
}
#linkremove {
background-image: url(<%= asset_data_uri 'remove.png' %>);
background-repeat: no-repeat;
background-position: center center;
width: 24px;
height: 24px;
position: absolute;
top: 3px;
right: 0;
cursor: pointer;
}
.cardSettings {
position: absolute;
left: 12px;

View file

@ -1,254 +0,0 @@
$attachment_button_size: 32px;
.attachments {
border-top: 1px solid #bdbdbd;
position: relative;
min-height: 3em;
.file {
margin-top: 0.75em;
max-width: 85%;
.filetype-icon {
width: 16px;
height: 16px;
padding: 0.5em;
padding-top: 0;
float: left;
}
}
}
.upload-audio-start,
.upload-file-dropzone,
.upload-photo-dropzone,
.CardOnGraph .attachment-type-chooser > div {
text-align: center;
color: #cccccc;
font-size: 12px;
cursor: pointer;
&:hover {
color: #999999;
}
&.photo-upload > div {
background-image: url(<%= asset_path('upload_icons/CameraIcons.png') %>);
}
&.link-upload > div {
background-image: url(<%= asset_path('upload_icons/LinkIcons.png') %>);
}
&.audio-upload > div {
background-image: url(<%= asset_path('upload_icons/MicIcons.png') %>);
}
&.file-upload > div {
background-image: url(<%= asset_path('upload_icons/CloudIcons.png') %>);
}
}
.photo-upload,
.link-upload,
.audio-upload,
.file-upload {
background-repeat: no-repeat;
background-size: $attachment_button_size $attachment_button_size;
background-position: 0 center;
width: $attachment_button_size;
height: $attachment_button_size;
& > div {
width: $attachment_button_size;
height: $attachment_button_size;
margin: 0 auto;
box-sizing: border-box;
padding-top: $attachment_button_size;
background-size: $attachment_button_size;
&:hover {
background-position: 0 $attachment_button_size;
}
}
}
.upload-audio-start,
.upload-file-dropzone,
.upload-photo-dropzone {
padding-top: 0.75em;
}
.upload-audio-recording {
font-size: small;
color: #aaa;
text-align: center;
}
$recording_button_size: 48px;
.upload-audio-stop {
background-image: url(<%= asset_path('recording-button.png') %>);
background-size: $recording_button_size;
width: $recording_button_size;
height: $recording_button_size;
margin: 0 auto;
cursor: pointer;
}
.CardOnGraph .attachment-type-chooser {
padding-top: .75em;
& > div {
display: inline-block;
width: 25%;
}
}
#embedlyLinkLoader {
margin: 0 auto;
width: 28px;
}
.CardOnGraph .link-chooser {
width:100%;
height:47px;
position: relative;
}
.link-chooser a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin-left: 40px;
padding-top:9px;
font-size: 16px;
line-height: 16px;
}
#addlink, #addupload {
display: inline-block;
width: 102px;
height: 12px;
text-align: left;
padding: 18px 0 18px 48px;
font-size: 12px;
color: #9e9e9e;
cursor: pointer;
position: relative;
}
#addlink:hover, #addupload:hover {
color: #616161;
}
.attachmentIcon {
background-repeat: no-repeat;
background-position: 0 0;
width: 24px;
height: 24px;
position: absolute;
top: 12px;
left: 12px;
}
#linkIcon {
background-image: url(<%= asset_data_uri('link_sprite.png') %>);
}
#uploadIcon {
background-image: url(<%= asset_data_uri('upload_sprite.png') %>);
}
#addlink:hover #linkIcon, #addupload:hover #uploadIcon {
background-position: 0 -24px;
}
.addLink {
position: relative;
}
#addLinkInput {
height: 32px;
width: 268px;
padding: 8px 16px 8px 16px;
position: relative;
border: none;
line-height: 14px;
}
#addLinkInput input{
padding: 9px 27px 9px 31px;
height: 12px;
width: 210px;
margin: 0 0 0 0;
border: none;
outline: none;
font-size: 12px;
line-height: 12px;
background: white;
color: black;
font-family: 'din-regular', helvetica, sans-serif;
}
#addLinkIcon {
position: absolute;
top: 12px;
left: 20px;
width: 24px;
height: 24px;
background-repeat: no-repeat;
background-position: 0 0;
background-image: url(<%= asset_data_uri('link_sprite.png') %>);
pointer-events: none;
z-index: 1;
}
.attachment-cancel {
position: absolute;
top: 8px;
right: 15px;
width: 32px;
height: 32px;
cursor: pointer;
float:none;
background-image: url(<%= asset_data_uri('remove.png') %>);
background-repeat: no-repeat;
background-position: center center;
}
.embeds.nonEmbedlyLink {
padding-top: 24px;
}
#embedlyLink {
border-left: 8px solid #CCC;
overflow: hidden;
padding: 8px;
padding-left: 12px;
-moz-box-shadow: 1px 1px 5px 0 #ccc;
-webkit-box-shadow: 1px 1px 5px 0 #ccc;
box-shadow: 1px 1px 5px 0 #ccc;
-moz-border-radius-topright: 5px;
-webkit-border-top-right-radius: 5px;
border-top-right-radius: 5px;
-moz-border-radius-bottomright: 8px;
-webkit-border-bottom-right-radius: 8px;
border-bottom-right-radius: 8px;
margin: 8px;
}
.linkActions {
position: relative;
}
.CardOnGraph .embeds {
position: relative;
overflow: hidden;
}
#linkremove {
background-image: url(<%= asset_data_uri 'remove.png' %>);
background-repeat: no-repeat;
background-position: center center;
width: 24px;
height: 24px;
position: absolute;
top: 3px;
right: 0;
cursor: pointer;
}

View file

@ -1,11 +0,0 @@
# frozen_string_literal: true
module Api
module V2
class AttachmentsController < RestfulController
def searchable_columns
[:file]
end
end
end
end

View file

@ -1,37 +0,0 @@
# frozen_string_literal: true
class AttachmentsController < ApplicationController
before_action :set_attachment, only: [:destroy]
after_action :verify_authorized
def create
@attachment = Attachment.new(attachment_params)
authorize @attachment
respond_to do |format|
if @attachment.save
format.json { render json: @attachment, status: :created }
else
format.json { render json: @attachment.errors, status: :unprocessable_entity }
end
end
end
def destroy
@attachment.destroy
respond_to do |format|
format.json { head :no_content }
end
end
private
def set_attachment
@attachment = Attachment.find(params[:id])
authorize @attachment
end
def attachment_params
params.require(:attachment).permit(:id, :file, :attachable_id, :attachable_type)
end
end

View file

@ -42,7 +42,7 @@ class UsersController < ApplicationController
correct_pass = @user.valid_password?(params[:current_password])
if correct_pass && @user.update_attributes(user_params)
update_follow_settings(@user, params[:settings]) if is_tester(@user)
update_follow_settings(@user, params[:settings])
@user.image = nil if params[:remove_image] == '1'
@user.save
sign_in(@user, bypass: true)

View file

@ -15,9 +15,7 @@ class Attachment < ApplicationRecord
end
}
validates :attachable, presence: true
validates_attachment_content_type :file, content_type: Attachable.allowed_types
validates_attachment_size :file, :in => 0.megabytes..5.megabytes
def image?
Attachable.image_types.include?(file.instance.file_content_type)

View file

@ -33,8 +33,7 @@ module Attachable
end
def audio_types
# .ogg files might be labelled as video
['audio/ogg', 'video/ogg', 'audio/mpeg', 'audio/wav', 'video/webm']
['audio/ogg', 'audio/mp3']
end
def text_types

View file

@ -69,23 +69,10 @@ class Topic < ApplicationRecord
Pundit.policy_scope(user, maps).map(&:id)
end
def attachments_json
attachments.map do |a|
{
id: a.id,
file_name: a.file_file_name,
content_type: a.file_content_type,
file_size: a.file_file_size,
url: a.file.url
}
end
end
def as_json(options = {})
super(methods: %i[user_name user_image collaborator_ids])
.merge(inmaps: inmaps(options[:user]), inmapsLinks: inmaps_links(options[:user]),
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]),
attachments: attachments_json)
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]))
end
def as_rdf

View file

@ -1,21 +0,0 @@
# frozen_string_literal: true
class AttachmentPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(attachable: TopicPolicy::Scope.new(user, Topic).resolve)
end
end
def index?
true
end
def create?
Pundit.policy(user, record.attachable).update?
end
def destroy?
Pundit.policy(user, record.attachable).update?
end
end

View file

@ -1,14 +0,0 @@
# frozen_string_literal: true
module Api
module V2
class AttachmentSerializer < ApplicationSerializer
attributes :id,
:file,
:attachable_type,
:attachable_id,
:created_at,
:updated_at
end
end
end

View file

@ -14,8 +14,7 @@ module Api
def self.embeddable
{
user: {},
metacode: {},
attachments: {}
metacode: {}
}
end

View file

@ -7,8 +7,6 @@ Metamaps::Application.routes.draw do
root to: 'main#home', via: :get
get 'request', to: 'main#requestinvite', as: :request
resources :attachments, only: %i[create destroy], shallow: true
namespace :explore do
get 'active'
get 'featured'
@ -123,7 +121,6 @@ Metamaps::Application.routes.draw do
namespace :api, path: '/api', default: { format: :json } do
namespace :v2, path: '/v2' do
resources :attachments, only: %i[index show]
resources :metacodes, only: %i[index show]
resources :mappings, only: %i[index create show update destroy]
resources :maps, only: %i[index create show update destroy] do

View file

@ -26,13 +26,13 @@ At the time of writing, there are four directories in the spec folder. One,
`support`, is for helper functions. `rails_helper.rb` and `spec_helper.rb` are
also for helper functions.
`factories` is for a gem called [factory-girl][factory-girl]. This gem lets you
`factories` is for a gem called [factory-bot][factory_bot]. This gem lets you
use the `create` and `build` functions to quickly create the simplest possible
valid version of a given model. For instance:
let(:map1) { create :map }
let(:ronald) { create :user, name: "Ronald" }
let(:map2) { create :map, user: ronald }
let(:alex) { create :user, name: "Alex" }
let(:map2) { create :map, user: alex }
As you can see, you can also customize the factories. You can read the full
documentation at the link above or check the existing specs to see how it works.
@ -53,5 +53,5 @@ the added code works. This will help in a few ways:
Happy testing!
[factory-girl]: https://github.com/thoughtbot/factory_girl
[factory-bot]: https://github.com/thoughtbot/factory_bot
[rspec-docs]: http://rspec.info

View file

@ -22,7 +22,6 @@ traits:
searchable: !include traits/searchable.raml
schemas:
attachment: !include schemas/_attachment.json
map: !include schemas/_map.json
mapping: !include schemas/_mapping.json
metacode: !include schemas/_metacode.json
@ -36,7 +35,6 @@ schemas:
# item: !include resourceTypes/item.raml
# collection: !include resourceTypes/collection.raml
/attachments: !include apis/attachments.raml
/maps: !include apis/maps.raml
/mappings: !include apis/mappings.raml
/metacodes: !include apis/metacodes.raml

View file

@ -1,16 +0,0 @@
get:
is: [ searchable: { searchFields: "file" }, orderable, pageable ]
securedBy: [ null, token, oauth_2_0 ]
responses:
200:
body:
application/json:
example: !include ../examples/attachments.json
/{id}:
get:
securedBy: [ null, token, oauth_2_0 ]
responses:
200:
body:
application/json:
example: !include ../examples/attachment.json

View file

@ -1,10 +0,0 @@
{
"data": {
"id": 1,
"file": "https://example.org/file.png",
"attachable_type": "Topic",
"attachable_id": 187,
"created_at": "2017-03-01T05:48:09.533Z",
"updated_at": "2017-03-01T05:48:09.533Z"
}
}

View file

@ -1,28 +0,0 @@
{
"data": [
{
"id": 1,
"file": "https://example.org/file.png",
"attachable_type": "Topic",
"attachable_id": 187,
"created_at": "2017-03-01T05:48:09.533Z",
"updated_at": "2017-03-01T05:48:09.533Z"
},
{
"id": 2,
"file": "https://example.org/file.docx",
"attachable_type": "Message",
"attachable_id": 1043,
"created_at": "2017-03-01T05:50:19.533Z",
"updated_at": "2017-03-01T05:50:19.533Z"
}
],
"page": {
"current_page": 1,
"next_page": 2,
"prev_page": 0,
"total_pages": 156,
"total_count": 312,
"per": 2
}
}

View file

@ -1,34 +0,0 @@
{
"name": "Attachment",
"type": "object",
"properties": {
"id": {
"$ref": "_id.json"
},
"file": {
"format": "uri",
"type": "string"
},
"attachable_type": {
"pattern": "(Topic|Message)",
"type": "string"
},
"attachable_id": {
"$ref": "_id.json"
},
"created_at": {
"$ref": "_datetimestamp.json"
},
"updated_at": {
"$ref": "_datetimestamp.json"
}
},
"required": [
"id",
"file",
"attachable_type",
"attachable_id",
"created_at",
"updated_at"
]
}

View file

@ -1,12 +0,0 @@
{
"name": "Attachment Envelope",
"type": "object",
"properties": {
"data": {
"$ref": "_attachment.json"
}
},
"required": [
"data"
]
}

View file

@ -1,19 +0,0 @@
{
"name": "Attachments",
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "_attachment.json"
}
},
"page": {
"$ref": "_page.json"
}
},
"required": [
"data",
"page"
]
}

View file

@ -37,14 +37,12 @@ const ReactApp = {
mobileTitle: '',
mobileTitleWidth: 0,
metacodeSets: [],
attachmentFileTypeIcons: {},
init: function(serverData, openLightbox) {
const self = ReactApp
self.serverData = serverData
self.mobileTitle = serverData.mobileTitle
self.openLightbox = openLightbox
self.metacodeSets = serverData.metacodeSets
self.attachmentFileTypeIcons = serverData.attachmentFileTypeIcons
routes = makeRoutes(serverData.ActiveMapper)
self.resize()
window && window.addEventListener('resize', self.resize)
@ -156,13 +154,10 @@ const ReactApp = {
getTopicCardProps: function() {
const self = ReactApp
return {
metacodeSets: self.metacodeSets,
onTopicFollow: Topic.onTopicFollow,
openTopic: TopicCard.openTopic,
metacodeSets: self.metacodeSets,
updateTopic: (topic, obj) => topic.save(obj),
uploadAttachment: TopicCard.uploadAttachment,
removeAttachment: TopicCard.removeAttachment,
fileTypeIcons: self.attachmentFileTypeIcons
onTopicFollow: Topic.onTopicFollow
}
},
getContextMenuProps: function() {

View file

@ -1,5 +1,3 @@
/* global $ */
import { ReactApp } from '../GlobalUI'
const TopicCard = {
@ -11,56 +9,6 @@ const TopicCard = {
hideCard: function() {
TopicCard.openTopic = null
ReactApp.render()
},
uploadAttachment: (topic, file) => {
const data = new window.FormData()
data.append('attachment[file]', file)
data.append('attachment[attachable_type]', 'Topic')
data.append('attachment[attachable_id]', topic.id)
return new Promise((resolve, reject) => {
$.ajax({
url: '/attachments',
type: 'POST',
data,
processData: false,
contentType: false,
success: (data) => {
console.log('file upolad success', data)
topic.fetch({ success: () => {
ReactApp.render()
resolve(true)
}})
},
error: (error) => {
console.error(error)
window.alert('File upload failed')
topic.fetch({ success: () => {
ReactApp.render()
resolve(false)
}})
}
})
})
},
removeAttachment: (topic) => {
const attachments = topic.get('attachments')
if (!attachments || attachments.length < 1) {
return
}
$.ajax({
url: `/attachments/${attachments[0].id}`,
type: 'DELETE',
success: () => {
console.log('delete success, syncing topic')
topic.fetch({ success: () => ReactApp.render() })
},
error: error => {
console.error(error)
window.alert('Failed to remove attachment')
topic.fetch({ success: () => ReactApp.render() })
}
})
}
}

View file

@ -1,101 +1,20 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import EmbedlyLinkChooser from './EmbedlyLinkChooser'
import EmbedlyCard from './EmbedlyCard'
import FileUploader from './FileUploader'
import PhotoUploader from './PhotoUploader'
import AudioUploader from './AudioUploader'
import FileAttachment from './FileAttachment'
import EmbedlyLink from './EmbedlyLink'
class Attachments extends Component {
constructor(props) {
super(props)
this.state = this.defaultState
}
defaultState = {
addingPhoto: false,
addingLink: false,
addingAudio: false,
addingFile: false
}
clearState = () => {
this.setState(this.defaultState)
}
// onClick handler for the 4 buttons, which triggers showing the proper uploader
choose = key => () => {
this.setState(Object.assign({}, this.defaultState, { [key]: true }))
}
render = () => {
const { topic, authorizedToEdit, updateTopic } = this.props
const link = topic.get('link')
const attachments = topic.get('attachments')
const file = attachments && attachments.length ? attachments[0] : null
let childComponent
if (link) {
childComponent = (
<EmbedlyCard link={link}
authorizedToEdit={authorizedToEdit}
removeLink={this.clearState}
/>
)
} else if (file) {
childComponent = (
<FileAttachment file={file}
authorizedToEdit={authorizedToEdit}
removeAttachment={this.props.removeAttachment}
fileTypeIcons={this.props.fileTypeIcons}
/>
)
} else if (!authorizedToEdit) {
childComponent = null
} else if (this.state.addingPhoto) {
childComponent = (
<PhotoUploader updateTopic={updateTopic}
uploadAttachment={this.props.uploadAttachment}
cancel={this.clearState}
/>
)
} else if (this.state.addingLink) {
childComponent = (
<EmbedlyLinkChooser updateTopic={updateTopic}
cancel={this.clearState}
/>
)
} else if (this.state.addingAudio) {
childComponent = (
<AudioUploader updateTopic={updateTopic}
uploadAttachment={this.props.uploadAttachment}
cancel={this.clearState}
/>
)
} else if (this.state.addingFile) {
childComponent = (
<FileUploader updateTopic={updateTopic}
uploadAttachment={this.props.uploadAttachment}
cancel={this.clearState}
/>
)
} else {
childComponent = (
<div className="attachment-type-chooser">
<div className="photo-upload"><div onClick={this.choose('addingPhoto')}>Photo</div></div>
<div className="link-upload"><div onClick={this.choose('addingLink')}>Link</div></div>
<div className="audio-upload"><div onClick={this.choose('addingAudio')}>Audio</div></div>
<div className="file-upload"><div onClick={this.choose('addingFile')}>Upload</div></div>
</div>
)
}
return (
<div className="attachments">
{childComponent}
<EmbedlyLink topicId={topic.id}
link={link}
authorizedToEdit={authorizedToEdit}
updateTopic={updateTopic}
/>
</div>
)
}
@ -104,10 +23,7 @@ class Attachments extends Component {
Attachments.propTypes = {
topic: PropTypes.object, // Backbone object
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func,
uploadAttachment: PropTypes.func,
removeAttachment: PropTypes.func,
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
updateTopic: PropTypes.func
}
export default Attachments

View file

@ -1,81 +0,0 @@
import React, { Component, PropTypes } from 'react'
import Recorder from 'react-recorder'
class AudioUploader extends Component {
constructor(props) {
super(props)
this.state = {
command: 'none'
}
}
timeLimit30sTimeoutId = null
enforce30sTimeLimit = cmd => {
window.clearTimeout(this.timeLimit30sTimeoutId)
if (cmd === 'start') {
this.timeLimit30sTimeoutId = window.setTimeout(() => {
this.command('stop')()
}, 30000)
}
}
command = cmd => () => {
this.enforce30sTimeLimit(cmd)
this.setState({ command: cmd })
}
onStop = blob => {
const now = new Date()
const date = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-${now.getHours()}:${now.getMinutes()}`
const filename = `metamaps-recorded-audio-${date}.wav`
const file = new window.File([blob], filename, { lastModifiedDate: now })
this.props.uploadAttachment(file).then(success => {
if (!success) {
this.command('none')
}
})
}
handleRecordingError = () => {
window.alert(`Audio recording failed. Some possible reasons include:
not using an HTTPS connection,
missing microphone,
you haven't allowed your browser access to your microphone,
or you need to reload the page.`)
}
render() {
return (
<div className="audio-uploader">
<Recorder command={this.state.command}
onStop={this.onStop}
onError={this.handleRecordingError}
/>
{this.state.command === 'start' && (
<div className="upload-audio-recording">
<div className="stop upload-audio-stop" onClick={this.command('stop')} />
<div className="upload-audio-recording-text">&nbsp;&nbsp;Recording...</div>
</div>
)}
{this.state.command === 'none' && (
<div className="start upload-audio-start" onClick={this.command('start')}>
Click to record <br />
(max 30 seconds)
</div>
)}
<div className="attachment-cancel" onClick={this.props.cancel} />
</div>
)
}
}
AudioUploader.propTypes = {
uploadAttachment: PropTypes.func,
cancel: PropTypes.func
}
export default AudioUploader

View file

@ -1,4 +1,4 @@
/* global embedly */
/* global $, embedly */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
@ -38,33 +38,29 @@ class EmbedlyCard extends Component {
}
render = () => {
const { link } = this.props
const { embedlyLinkLoaded, embedlyLinkStarted, embedlyLinkError } = this.state
const notReady = embedlyLinkStarted && !embedlyLinkLoaded && !embedlyLinkError
return (
<div className="embeds">
<div>
<a style={{ display: notReady ? 'none' : 'block' }}
href={this.props.link}
href={link}
id="embedlyLink"
target="_blank"
data-card-description="0"
>
{this.props.link}
{link}
</a>
{notReady && <div id="embedlyLinkLoader">loading...</div>}
{this.props.authorizedToEdit && (
<div id="linkremove" onClick={this.props.removeLink} />
)}
</div>
)
}
}
EmbedlyCard.propTypes = {
link: PropTypes.string,
authorizedToEdit: PropTypes.bool,
removeLink: PropTypes.func
link: PropTypes.string
}
export default EmbedlyCard

View file

@ -1,7 +1,10 @@
/* global embedly */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class EmbedlyLinkChooser extends Component {
import Card from './Card'
class EmbedlyLink extends Component {
constructor(props) {
super(props)
@ -10,9 +13,12 @@ class EmbedlyLinkChooser extends Component {
}
}
removeLink = () => {
this.props.updateTopic({ link: null })
}
resetLink = () => {
this.setState({ linkEdit: '' })
this.props.cancel()
}
onLinkChangeHandler = e => {
@ -29,11 +35,17 @@ class EmbedlyLinkChooser extends Component {
}
render = () => {
const { link, authorizedToEdit, topicId } = this.props
const { linkEdit } = this.state
const hasAttachment = !!link
if (!hasAttachment && !authorizedToEdit) return null
return (
<div className="link-chooser">
<div className="addLink">
<div className={hasAttachment ? 'embeds' : 'link-adder'}>
<div className="addLink"
style={{ display: hasAttachment ? 'none' : 'block' }}
>
<div id="addLinkIcon"></div>
<div id="addLinkInput">
<input ref={input => (this.linkInput = input)}
@ -41,17 +53,26 @@ class EmbedlyLinkChooser extends Component {
value={linkEdit}
onChange={this.onLinkChangeHandler}
onKeyUp={this.onLinkKeyUpHandler}></input>
<div className="attachment-cancel" onClick={this.resetLink}></div>
{linkEdit && <div id="addLinkReset" onClick={this.resetLink}></div>}
</div>
</div>
{link && <Card key={topicId} link={link} />}
{authorizedToEdit && (
<div id="linkremove"
style={{ display: hasAttachment ? 'block' : 'none' }}
onClick={this.removeLink}
/>
)}
</div>
)
}
}
EmbedlyLinkChooser.propTypes = {
updateTopic: PropTypes.func,
cancel: PropTypes.func
EmbedlyLink.propTypes = {
topicId: PropTypes.number,
link: PropTypes.string,
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func
}
export default EmbedlyLinkChooser
export default EmbedlyLink

View file

@ -1,57 +0,0 @@
import React, { Component, PropTypes } from 'react'
class FileAttachment extends Component {
getFileType = contentType => {
if (contentType === 'text/plain') {
return 'text'
} else if (contentType === 'application/pdf') {
return 'pdf'
} else if (contentType.match(/^image\//)) {
return 'image'
} else if (contentType.match(/^audio\//) ||
contentType === 'video/ogg' ||
contentType === 'video/webm') {
return 'audio'
} else {
return 'unknown'
}
}
getFileIcon = file => {
const type = this.getFileType(file.content_type)
if (this.props.fileTypeIcons[type]) {
return this.props.fileTypeIcons[type]
} else {
return this.props.fileTypeIcons.unknown
}
}
render() {
const { file } = this.props
return (
<div className={`file ${this.getFileType(file.content_type)}-file-type`}
style={{ clear: 'both' }}
>
<a href={file.url} target="_blank">
<img src={this.getFileIcon(file)} className="filetype-icon" />
{file.file_name}
</a>
<div className="attachment-cancel" onClick={this.props.removeAttachment} />
</div>
)
}
}
FileAttachment.propTypes = {
file: PropTypes.shape({
content_type: PropTypes.string,
file_name: PropTypes.string,
url: PropTypes.string
}),
authorizedToEdit: PropTypes.bool,
removeAttachment: PropTypes.func,
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
}
export default FileAttachment

View file

@ -1,34 +0,0 @@
import React, { Component, PropTypes } from 'react'
import Dropzone from 'react-dropzone'
class FileUploader extends Component {
handleFileUpload = (acceptedFiles, rejectedFiles) => {
if (acceptedFiles.length >= 1) {
this.props.uploadAttachment(acceptedFiles[0])
} else {
window.alert('File upload failed, please try again.')
}
}
render() {
return (
<div className="upload-file">
<Dropzone className="upload-file-dropzone"
onDrop={this.handleFileUpload}
>
Drag file here <br />
(maximum 5mb)
</Dropzone>
<div className="attachment-cancel" onClick={this.props.cancel} />
</div>
)
}
}
FileUploader.propTypes = {
updateTopic: PropTypes.func,
uploadAttachment: PropTypes.func,
cancel: PropTypes.func
}
export default FileUploader

View file

@ -1,34 +0,0 @@
import React, { Component, PropTypes } from 'react'
import Dropzone from 'react-dropzone'
class PhotoUploader extends Component {
handleFileUpload = (acceptedFiles, rejectedFiles) => {
if (acceptedFiles.length >= 1) {
this.props.uploadAttachment(acceptedFiles[0])
} else {
window.alert('File upload failed, please try again.')
}
}
render() {
return (
<div className="upload-photo">
<Dropzone className="upload-photo-dropzone"
onDrop={this.handleFileUpload}
>
Drag photo here <br />
or click to upload
</Dropzone>
<div className="attachment-cancel" onClick={this.props.cancel} />
</div>
)
}
}
PhotoUploader.propTypes = {
updateTopic: PropTypes.func,
uploadAttachment: PropTypes.func,
cancel: PropTypes.func
}
export default PhotoUploader

View file

@ -10,17 +10,12 @@ import Info from './Info'
class ReactTopicCard extends Component {
render = () => {
const {
currentUser, onTopicFollow, updateTopic, uploadAttachment,
removeAttachment
} = this.props
const { currentUser, onTopicFollow, updateTopic } = this.props
const topic = this.props.openTopic
if (!topic) return null
const wrappedUpdateTopic = obj => updateTopic(topic, obj)
const wrappedUploadAttachment = file => uploadAttachment(topic, file)
const wrappedRemoveAttachment = () => removeAttachment(topic)
const authorizedToEdit = topic.authorizeToEdit(currentUser)
const hasAttachment = topic.get('link') && topic.get('link') !== ''
@ -53,13 +48,9 @@ class ReactTopicCard extends Component {
authorizedToEdit={authorizedToEdit}
onChange={wrappedUpdateTopic}
/>
<Attachments key={topic.id}
topic={topic}
<Attachments topic={topic}
authorizedToEdit={authorizedToEdit}
updateTopic={wrappedUpdateTopic}
uploadAttachment={wrappedUploadAttachment}
removeAttachment={wrappedRemoveAttachment}
fileTypeIcons={this.props.fileTypeIcons}
/>
<Info topic={topic} />
<div className='clearfloat' />
@ -84,9 +75,7 @@ ReactTopicCard.propTypes = {
name: PropTypes.string
}))
})),
redrawCanvas: PropTypes.func,
uploadAttachment: PropTypes.func,
fileTypeIcons: PropTypes.objectOf(PropTypes.string)
redrawCanvas: PropTypes.func
}
export default ReactTopicCard

View file

@ -49,7 +49,6 @@
"react-draggable": "3.0.3",
"react-dropzone": "4.1.2",
"react-onclickoutside": "6.5.0",
"react-recorder": "1.0.0",
"react-router": "3.0.5",
"redux": "3.7.2",
"riek": "1.1.0",

View file

@ -1,38 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'topics API', type: :request do
let(:user) { create(:user, admin: true) }
let(:token) { create(:token, user: user).token }
let(:attachment) { create(:attachment) }
it 'GET /api/v2/attachments' do
create_list(:attachment, 5)
get '/api/v2/attachments', params: { access_token: token }
expect(response).to have_http_status(:success)
expect(response).to match_json_schema(:attachments)
expect(JSON.parse(response.body)['data'].count).to eq 5
end
it 'GET /api/v2/attachments/:id' do
get "/api/v2/attachments/#{attachment.id}"
expect(response).to have_http_status(:success)
expect(response).to match_json_schema(:attachment)
expect(JSON.parse(response.body)['data']['id']).to eq attachment.id
end
context 'RAML example' do
let(:resource) { get_json_example(:attachment) }
let(:collection) { get_json_example(:attachments) }
it 'resource matches schema' do
expect(resource).to match_json_schema(:attachment)
end
it 'collection matches schema' do
expect(collection).to match_json_schema(:attachments)
end
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :access_request do
map
user

View file

@ -1,6 +0,0 @@
# frozen_string_literal: true
FactoryGirl.define do
factory :attachment do
association :attachable, factory: :topic
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :mapping do
xloc 0
yloc 0

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :map do
sequence(:name) { |n| "Cool Map ##{n}" }
permission :commons

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :message do
association :resource, factory: :map
user

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :metacode do
sequence(:name) { |n| "Cool Metacode ##{n}" }
manual_icon 'https://images.com/image.png'

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :star do
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :synapse do
sequence(:desc) { |n| "Cool synapse ##{n}" }
category :'from-to'

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :token do
user
description ''

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :topic do
user
association :updated_by, factory: :user

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
FactoryGirl.define do
FactoryBot.define do
factory :user_map do
map
user

View file

@ -10,7 +10,7 @@
# have actual codes, you'll need to specify one simple_user and then you
# can specify other :code_user users based on the pre-existing user's code.
FactoryGirl.define do
FactoryBot.define do
factory :code_user, class: User do
sequence(:name) { |n| "Cool User ##{n}" }
sequence(:email) { |n| "cooluser#{n}@cooldomain.com" }

View file

@ -23,7 +23,7 @@ RSpec.describe MapPolicy, type: :policy do
context 'private' do
let(:map) { create(:map, permission: :private) }
permissions :show?, :create?, :update?, :destroy? do
it 'denies access' do
it 'permits access' do
expect(subject).to_not permit(nil, map)
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
# lets you type create(:user) instead of FactoryGirl.create(:user)
# lets you type create(:user) instead of FactoryBot.create(:user)
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
config.include FactoryBot::Syntax::Methods
end