Mongoid Polymorphic Association + FactoryGirl + RSpec - mongoid

I'm trying to embed emails inside person Mongoid object using polymorphic approach. Getting "BSON::InvalidDocument: Cannot serialize an object of class Mongoid::Relations::Embedded::Many into BSON." whenever I run the test. Please see code below and any will be greatly appreciated. I'm not sure what is correct way to build emails inside person in FactoryGirl. Thanks.
class Email
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :mailable, polymorphic: true
field :email, type: String
field :category, type: String
end
class Person
include Mongoid::Document
embeds_many :emails, as: :mailable #polymorhpic
index "emails.email", unique: true
field :first_name, type: String
field :middle_name, type: String
field :last_name, type: String
validates_uniqueness_of :emails
end
FactoryGirl.define do
sequence(:fn) {|n| "first_name#{n}" }
sequence(:ln) {|n| "last_name#{n}" }
factory :person do
first_name { generate(:fn) }
last_name { generate(:ln) }
gender 'M'
nationality 'USA'
ssn '123-88-1111'
factory :emails_ do
emails { Factory.build(:email) }
end
end
end
FactoryGirl.define do
sequence(:address) {|n| "user#{n}#mail.com" }
factory :email do
email { generate(:address) }
category 'personal'
end
end

Here's the approach I took when last using Factory Girl with embedded associations in mongoid. Try this in your user factory instead.
emails { |e| [e.association(:email)] }

Related

Rails 5 Api model not saving fields

I have a rails 5.2 API that currently creates and authenticates users (tested in Postman). However, I have added another model called Stories and when I attempt to create a new Story, a story is created but only the id attribute is saved.
Here is my StoriesController:
class StoriesController < ApplicationController
before_action :set_story, only: [:show, :update, :destroy]
# GET /stories
def index
#stories = Story.all
render json: #stories
end
# POST /stories
def create
#story = Story.new(story_params)
if #story.save
render json: #story, status: :created
else
render json: { errors: #story.errors.full_messages },
status: :unprocessable_entity
end
end
# GET /stories/:id
def show
render json: #story
end
# PUT /stories/:id
def update
#story.update(story_params)
head :no_content
end
# DELETE /stories/:id
def destroy
#story.destroy
head :no_content
end
private
def story_params
# whitelist params
params.permit(:title, :category, :summary)
end
def set_story
#story = Story.find(params[:id])
end
end
Here is the request in Postman:
Here is the output in Postman:
I thought this was a params issue, but that does not seem to be it.
man there is an extra opening bracket on your post payload on line 3.
The right json payload is.:
{
"story": {
"title": "Spiderman"
"category": "Superhero"
"summary": "new spiderguy ftw"
}
}
I think there is a issue with your story_params method. The Story attributes "tittle", "category" and "summary" are nested in "story".
You can try this:
params.require(:story).permit(:title, :category, :summary)

Django throws column cannot be null error when POSTing to REST Framework

Django REST Framework is reporting an error that a value is null even though I am sending the value when I POST the data.
The error Django reports is:
django.db.utils.IntegrityError: (1048, "Column 'owner_id' cannot be null")
[04/Apr/2016 18:40:58] "POST /api/items/ HTTP/1.1" 500 226814
The Angular 2 code that POSTs to Django REST Framework API is:
let body = JSON.stringify({ url: 'fred', item_type: 'P', owner_id: 2 });
let headers = new Headers();
headers.append('Content-Type', 'application/json');
this.http.post('http://127.0.0.1:8000/api/items/',
body, {
headers: headers
})
.subscribe(
data => {
alert(JSON.stringify(data));
},
err => alert('POST ERROR: '+err.json().message),
() => alert('POST Complete')
);
My Django API view looks like this:
class ItemViewSet(viewsets.ModelViewSet):
queryset = Item.objects.all().order_by('-date_added')
serializer_class = ItemSerializer
"""
Use the API call query params to determing what to return
API params can be:
?user=<users_id>&num=<num_of_items_to_return>&from=<user_id_of_items_to_show>
"""
def get_queryset(self):
this_user = self.request.query_params.get('user', None)
restrict_to_items_from_user_id = self.request.query_params.get('from', None)
quantity = self.request.query_params.get('num', 20)
if restrict_to_items_from_user_id is not None:
queryset = Item.objects.filter(owner=restrict_to_items_from_user_id, active=True).order_by('-date_added')[0:int(quantity)]
elif this_user is not None:
queryset = Item.objects.filter(active=True, credits_left__gt=0).exclude(pk__in=Seen.objects.filter(user_id=this_user).values_list('item_id', flat=True))[0:int(quantity)]
else:
queryset = Item.objects.filter(active=True, credits_left__gt=0)[0:int(quantity)]
print("User id param is %s and quantity is %s" % (user_id,quantity))
return queryset
The associated model is:
class Item(models.Model):
ITEM_TYPES = (
('V', 'Vine'),
('Y', 'YouTube'),
('P', 'Photo'), # Photo is stored by us on a CDN somewhere
('F', 'Flickr'),
('I', 'Instagram'),
('D', 'DeviantArt'),
('5', '500px'),
)
owner = models.ForeignKey(User, on_delete=models.CASCADE) # Id of user who owns the item
title = models.CharField(max_length=60, default='') # URL of where item resides (e.g. Vine or YouTube url)
url = models.CharField(max_length=250, default='') # URL of where item resides (e.g. Vine or YouTube url)
item_type = models.CharField(max_length=1, choices=ITEM_TYPES) # Type of item (e.g. Vine|YoutTube|Instagram|etc.)
keywords = models.ManyToManyField(Keyword, related_name='keywords')
# E.g. Art, Travel, Food, etc.
credits_applied = models.IntegerField(default=10, help_text='Total number of credits applied to this item including any given by VeeU admin')
# Records the total number of credits applied to the Item
credits_left = models.IntegerField(default=10, help_text='The number of credits still remaining to show the item')
# Number of credits left (goes down each time item is viewed
credits_gifted = models.IntegerField(default=0, help_text='The number of credits this item has been gifted by other users')
# Number of credits users have gifted to this item
date_added = models.DateTimeField(auto_now_add=True) # When item was added
liked = models.IntegerField(default=0) # Number of times this item has been liked
disliked = models.IntegerField(default=0) # Number of times this item has been disliked
active = models.BooleanField(default=True, help_text='If you mark this item inactive please say why in the comment field. E.g. "Inapproriate content"')
# True if item is available for showing
comment = models.CharField(max_length=100, blank=True) # Comment to be applied if item is inactive to say why
# Add defs here for model related functions
# This to allow url to be a clickable link
def item_url(self):
return u'%s' % (self.url, self.url)
item_url.allow_tags = True
def __str__(self):
return '%s: Title: %s, URL: %s' % (self.owner, self.title, self.url)
I can't see what is wrong with my POST call, or the Django code.
EDIT: Added serializer code
Here is the associated serializer
class ItemSerializer(serializers.HyperlinkedModelSerializer):
username = serializers.SerializerMethodField()
def get_username(self, obj):
value = str(obj.owner)
return value
def get_keywords(self, obj):
value = str(obj.keywords)
return value
class Meta:
model = Item
fields = ('url', 'item_type', 'title', 'credits_applied', 'credits_left', 'credits_gifted', 'username', 'liked', 'disliked')
Your serializer doesn't have an owner field and your view doesn't provide one. As it's non null in your model the DB will complain about this.
You should override the view's perform_update and add the owner as extra argument to the serializer
You need to pass the Resource URI for the field which is foreign key,
here owner is the FK, so the
ownerIns = User.objects.get(id=2)
let body = JSON.stringify({ url: 'fred', item_type: 'P', owner: ownerIns, owner_id: ownerIns.id });
I ran into a similar issue using Angular and Flask. This may because your CORS headers are not set correctly for your Django app which makes your Angular app not allowed to post to the backend. In Flask, I fixed it using this code:
#app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
return response
I'm not sure how to do this in Django, but this may be a great first stop for you since this is likely your issue.
Is your ItemSerializer code correct? The rest looks fine to me.
You should be having 'owner_id' in your serializer fields i think.
Take a look at this answer, add the related fields in this manner.
https://stackoverflow.com/a/20636415/5762482

Can't seem to use optional foreign key relationships when creating new object using Django+AngularJS

I'm using AngularJS on the front-end with Django managing the backend and API along with the Django REST framework package. I have a Project model which belongs to User and has many (optional) Intervals and Statements. I need to be able to create a 'blank' project and add any intervals/statements later, but I'm hitting a validation error when creating the project. Below are the relevant code sections.
Django model code (simplified):
class Project(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='projects', on_delete='models.CASCADE')
project_name = models.CharField(max_length=50)
class Statement(models.Model):
project = models.ForeignKey(Project, related_name='statements', on_delete='models.CASCADE', null=True, blank=True)
class Interval(models.Model):
project = models.ForeignKey(Project, related_name='intervals', on_delete='models.CASCADE', null=True, blank=True)
Django view code (simplified):
class ProjectList(APIView):
def post(self, request, format=None):
serializer = ProjectSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Angular controller code (simplified):
$scope.createProject = function(){
var projectData = {
"user": $scope.user.id,
"project_name": $scope.newProject.project_name
};
apiSrv.request('POST', 'projects', projectData,
function(data){},
function(err){}
);
};
Angular service code (simplified):
apiSrv.request = function(method, url, args, successFn, errorFn){
return $http({
method: method,
url: '/api/' + url + ".json",
data: JSON.stringify(args)
}).success(successFn);
};
Server response:
{"intervals":["This field is required."],"statements":["This field is required."]}
Am I missing something here? I should be able to create a project without a statement or interval, but I'm not able to. Thanks for any suggestions.
Edit: Added Relevant section from ProjectSerializer
class ProjectSerializer(serializers.ModelSerializer):
intervals = IntervalSerializer(many=True)
statements = StatementSerializer(many=True)
class Meta:
model = Project
fields = (
'id',
'project_name',
[removed extraneous project fields]
'user',
'intervals',
'statements'
)
You need to set the read_only attribute on the 'interval' and 'statements' fields
class ProjectSerializer(serializers.ModelSerializer):
intervals = IntervalSerializer(many=True, read_only=True)
statements = StatementSerializer(many=True, read_only=True)
class Meta:
model = Project
fields = ('id', 'project_name', 'user', 'intervals','statements')
or you can specify the read_only fields like this,
class Meta:
model = Project
fields = ('id', 'project_name', 'user', 'intervals','statements')
read_only_fields = ('intervals','statements')

add and remove existing models to a has_many relationship using nested attributes

In a rails 4 application I have two models :
class Gallery
include Mongoid::Document
has_many :thumbnails
end
class Thumbnail
include Mongoid::Document
belongs_to :gallery
end
I populated the mongodb database with a bunch of galleries with thumbnails in them and some unused thumbnails (with a nil gallery_id).
Now on the client side I use Marionette with backbone-associations and I represent the Gallery as so :
class Entities.Gallery extends Backbone.AssociatedModel
idAttribute: '_id'
urlRoot: '/galleries'
paramRoot: 'gallery'
relations: [
type: Backbone.Many
key: 'thumbnails'
remoteKey: 'thumbnails_attributes'
relatedModel: -> Entities.Thumbnail
]
initialize: ->
#on 'add:thumbnails', (thumbnail) => thumbnail.set 'gallery_id', #get('_id')
class Entities.Thumbnail extends Backbone.AssociatedModel
idAttribute: '_id'
But I also have a collection of unused thumbnails :
class Entities.UnusedThumbnails extends Backbone.Collection
model: Entities.Thumbnail
initialize: ->
#on 'add', (thumbnail) -> thumbnail.set 'gallery_id', null
I can move thumbnails around between the gallery and the UnusedThumbnails collection just fine, but how do I persists them ?
If I just add a thumbnail from the UnusedThumbnails collection to the gallery thumbnails and save the gallery using :
gallery.save([], patch: true)
i get a 404 response saying "Document(s) not found for class Thumbnail with id(s) ..." which make sense since rails only search for a thumbnail with this id inside the gallery.
Same for removing thumbnails from the gallery, if I post the gallery with missing thumbnails the rails update method will just assume these thumbnails are unchanged.
Do I need to save each added / removed thumbnails separately?
What's the proper way to do this ?
Edit:
I realize I'll probably need to create a specialized update action, like update_strict (for lack of a better name)
def update_strict
new_ids = gallery_params[:thumbnails_attributes].map(&:_id)
existing_ids = #gallery.thumbnails_ids
ids_to_add = new_ids - existing_ids
ids_to_remove = existing_ids - new_ids
#gallery.thumbnails.find(ids_to_remove).each |thumbnail| do
thumbnail.gallery = nil
thumbnail.save
end
ids_to_add.each |id| do
thumbnail = Thumbnail.find(id)
thumbnail_params = (gallery_params[:thumbnails_attributes].select { |t| t._id == id })[0]
thumbnail.update(thumbnail_params)
end
gallery_params[:thumbnails_attributes].delete_if { |thumbnail| ids_to_add.include?(thumbnail._id) }
respond_to do |format|
if #gallery.update(gallery_params)
format.html { redirect_to #gallery, notice: 'Gallery was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #gallery.errors, status: :unprocessable_entity }
end
end
end
But is there a proper, cleaner way ?
I finally ended up overwriting thumbnails_attributes= in the Gallery model :
def thumbnails_attributes=thumbnails_attributes)
ids = thumbnails_attributes.map { |t| t['id'] }
(ids - thumbnail_ids).each do |id|
thumbnail = Thumbnail.find id
thumbnails << thumbnail
end
super(thumbnails_attributes)
end
This allow me to add existing thumbnails to the gallery.
The simplest way to allow their removal was to switch to backbone-nested-attributes which add a _destroy attribute to destroyed models.

mongoid has_many children not saving through nested_attributes when Tire::Callbacks included

So here is the issue:
context:
mongoid (2.2.6)
tire (0.5.1)
classes:
class Account
include Mongoid::Document
has_many :comments, auto_save: true
accepts_nested_attributes_for :comments
end
class Comment
include Mongoid::Document
include Tire::Callbacks
include Tire::Search
belongs_to :account
end
operations:
account = Account.first
account.comments_attributes = [{content: 'super'}]
account.comments => [#<Comment content: 'super'>]
issue:
Comment.count => 0
Comment.search().count => 1
account.comments.build {content: 'super'}
account.save => true
Comment.count => 1
it seems that there is conflict with the autosave with nested_attributes change on mongoid and the callbacks of tire ...
the problem was linked to this one => https://stackoverflow.com/questions/14860594/issue-with-sti-nested-attributes-mongoid-3-0-22
Now it seems to be solved.

Resources