Extending the admin import form for django import_export - django-admin

I'm using Django import_export to implement CSV upload in my admin pages. Now I have one model, that contains a foreign key column, but the foreign key column will have only one value for each import. Therefore I would like to allow the user to choose the related model instance from a drop-down instead of forcing the user to append the columns themselves. In order to do this I need to customize the import form, which requires overriding the default methods import_action and process_import, but my efforts so far have shown no effect. Here is what I have so far:
from django import forms
from import_export.forms import ImportForm
from .models import MyModel, RelatedModel
class CustomImportForm(ImportForm):
"""Add a model choice field for a given model to the standard form."""
appended_instance = forms.ModelChoiceField(queryset=None)
def __init__(self, choice_model, import_formats, *args, **kwargs):
super(CustomImportForm, self).__init__(import_formats, *args, **kwargs)
self.fields['appended_instance'].queryset = choice_model.objects.all()
#admin.register(MyModel)
class MyModelAdmin(ImportExportModelAdmin):
resource_class = SomeResource
def import_action(self, request, *args, **kwargs):
super().import_action(self, request, *args, **kwargs)
form = CustomImportForm(RelatedModel,
import_formats,
request.POST or None,
request.FILES or None)
Now when I go the import page I get the AttributeError MyModelAdmin has no attribute 'POST' and in the local vars I can see that the request object is actually the MyModelAdmin class, which is I believe is not what it's supposed to be.

Avoid reimplementing either import_action() or process_import(); partially because they're fairly complex and fragile methods, but more importantly because there are neater ways of doing this using the existing hooks in the Import/Export API. See this answer for more details.

I know, this is an old post, but I ran into this, when looking on how to override the import_action.
Your error is here:
super().import_action(self, request, *args, **kwargs)
You should call it without the self:
super().import_action(request, *args, **kwargs)
or for older python:
super(MyModelAdmin, self).import_action(request, *args, **kwargs)

def import_action(self, request, *args, **kwargs):
response = super(MyModelAdmin, self).import_action(request, *args, **kwargs)
context = response.context_data
import_formats = self.get_import_formats()
context['form'] = CustomImportForm(RelatedModel, import_formats, request.POST or None, request.FILES or None)
return TemplateResponse(request, [self.import_template_name], context)

Related

Fiber local use case

I am not sure if I am doing this right, but here is my scenario:
I need to create a thread to do some API call and continue with normal operations while the API call is made. This part is fine. The issue arises when I want to join back in.
I am currently creating a thread in a different module and want to join back in a different module. Hence I am not able to use the reference to the thread created earlier.
To overcome this I did this: Thread.current[:ref_to_new_thread] = Thread.new { API CALL }
Finally I join back in a different module using - Thread.current[:ref_to_new_thread].join
Is this the right way? or is there a better way.
Minimal reproducible example:
File 1:
module A
module Aa
def my_method
#some actions
Thread.current[:ref_to_new_thread] = Thread.new { API CALL }
#some actions
end
end
end
File 2:
module B
module Bb
def my_second_method
#some actions
Thread.current[:ref_to_new_thread].join
Thread.current[:ref_to_new_thread] = nil
end
end
end
I am new to ruby on rails so my apologies.

rails: different methods for different users

I have a location field on my User model with two possible values, Spain and France (maybe more in the future). I need to make API calls for each user but call a different API depending on the user's location (also, parse the response differently). I have no idea what the best way to solve this is.
Here's more or less what my code looks like now:
class Api::Foobar
include HTTParty
base_uri 'http://demo.foobar.es/vweb/xml'
GLOBAL_OPTIONS = {edad: 'anciano', farmacovigilancia: false, deportista: false, administrativas: false}
def initialize(user)
#user = user
end
def contraindications
self.class.get("/ws_patient/alertas", query: GLOBAL_OPTIONS.merge(user_options) )
end
def self.medications_by_name(name)
response = get("/ws_drug/SearchByName", query: {value: name} )
response['object']['drug_set']['drug']
end
.....
end
I'm doing things like Api::Foobar.medications_by_name('asp') or Api::Foobar.new(User.first).contraindications
I'd like to build an abstraction layer that allows me to call the same methods for each user and automatically pick the appropriate class/module/api based on the users location. Can anyone point me in the right direction? Maybe something on best practices when building abstraction layers like this?
========== Edit ==========
I ended up namespacing the classes one more level and adding this method to the User class:
def foobar_api
if !self.location.nil?
Object.qualified_const_get("Api::Foobar::#{self.location.capitalize}").new(self)
else
Api::Foobar::Spain.new(self)
end
end
now I'm calling User.first.foobar_api.contraindications
Can anybody comment if this is a good idea? or if there is a better way
Have a perform method in the class which accepts the user object. And based on the user-location value, you could use one amongst the internal methods.

Carrierwave: file hash and model id in filename/store_dir

I'm using carrierwave in a Rails 4 project with the file storage for development and testing and the fog storage (for storing on Amazon S3) for production.
I would like to save my files with paths like this:
/model_class_name/part_of_hash/another_part_of_hash/hash-model_id.file_extension
(example: /images/12/34/1234567-89.png where 1234567 is the SHA1 hash of the file content and 89 is the id of the associated image model in the database).
What I tried so far is this:
class MyUploader < CarrierWave::Uploader::Base
def store_dir
"#{model.class.name.underscore}/#{sha1_for(file)[0..1]}/#{sha1_for(file)[2..3]}"
end
def filename
"#{sha1_for(file)}-#{model.id}.#{file.extension}" if original_file
end
private
def sha1_for file
Digest::SHA1.hexdigest file.read
end
end
This does not work because:
model.id is not available when filename is called
file is not always available when store_dir is called
So, coming to my questions:
is it possible to use model ids/attributes within filename? This link says it should not be done; is there a way to work around it?
is it possible to use file content/attributes within store_dir? I found no documentation on this but my experiences so far say "no" (see above).
how would you implement file/directory naming to get something as close as possible to what I outlined in the beginning?
Including the id in the filename on create may not be possible, since the filename is stored in the database but the id isn't available yet. An (admittedly rather extreme) workaround would be to use a temporary value on create, and then after_commit on: :create, move the file and change the name in the database. It may be possible to optimize this with an after_create, but I'll leave that up to you. (This is where carrierwave actually uploads the file.)
Including file attributes directly within the store_dir isn't possible, since store_dir is used to calculate the url—url would require knowing the sha1, which requires having access to the file, which requires knowing the url, etc. The workaround is pretty obvious: cache the attributes in which you're interested (in this case the sha1) in the model's database record, and use that in the store_dir.
The simpler variant on the id-in-filename approach is to use some other value, such as a uuid, and store that value in the database. There are some notes on that here.
Taavo's answer strictly answers my questions. But I want to quickly detail the final solution I implemented since it may helps someone else, too...
I gave up the idea to use the model id in the filename and replaced it with a random string instead (the whole idea of the model id in the filename was to just ensure that 2 identical files associated with different models end up with different file names; and some random characters ensure that as well).
So I ended up with filenames like filehash-randomstring.extension.
Since carrierwave saves the filename in the model, I realized that I already have the file hash available in the model (in the form of the first part of the filename). So I just used this within store_dir to generate a path in the form model_class_name/file_hash_part/another_file_hash_part.
My final implementation looks like this:
class MyUploader < Carrierwave::Uploader::Base
def store_dir
# file name saved on the model. It is in the form:
# filehash-randomstring.extension, see below...
filename = model.send(:"#{mounted_as}_identifier")
"#{model.class.name.underscore}/#{filename[0..1]}/#{filename[3..4]}"
end
def filename
if original_filename
existing = model.send(:"#{mounted_as}_identifier")
# reuse the existing file name from the model if present.
# otherwise, generate a new one (and cache it in an instance variable)
#generated_filename ||= if existing.present?
existing
else
"#{sha1_for file}-#{SecureRandom.hex(4)}.#{file.extension}"
end
end
end
private
def sha1_for file
Digest::SHA1.hexdigest file.read
end
end
I came across the same problem recently, where the model.id was not available yet when storing the filename in the DB, upon creation of the uploader record. I found this workaround. I am not sure if it is respecting RESTful principles, I am open to suggestions.
I modified the controller, so that right after the creation of the image, an update_attributes is executed, so that the filename including the now existing model.id value is saved in the DB.
def create
#uploader = Uploader.new(uploader_params)
if #uploader.save
if #uploader.update_attributes(uploader_params)
render json: #uploader, status: :created
end
else
render json: #uploader.errors, status: :unprocessable_entity
end
end

Race condition with rails sessions

There is an array inside the session hash which I'm adding things to it. The problem is, sometimes multiple requests get processed at the same time (because ajax), and the changes a request makes to the array is then replaced by the changes made by the second request.
Example, the array first looks like this:
[63, 73, 92]
Then the first request adds something to it:
[63, 73, 92, 84]
The second request does the same thing (but works on the older version obviously):
[63, 73, 92, 102]
So in the end the array doesn't look like it should. Is there a way to avoid that?
I tried to use the cache store, the active record store and the cookie store. Same problem with all of them.
Thanks.
Session race conditions are very common in Rails. Redis session store doesn't help either!
The reason is Rails only reads and creates the session object when it receives the request and writes it back to session store when request is complete and is about to be returned to user.
Java has a solution for this called session replication. We can build our own concurrent redis based session store. The following is pretty much it. Except the update method is missing the lock. But it almost never happens to get race condition in it.
To get session hash just use concurrent_session which returns a hash.
To set some value in it, use update_concurrent_session method. It deep merges the new hash into the old value:
class ApplicationController < ActionController::Base
def concurrent_session
#concurrent_session ||= get_concurrent_session
end
def update_concurrent_session h
return unless session.id.present?
#concurrent_session = get_concurrent_session.deep_merge(h)
redis.set session_key, #concurrent_session.to_json
redis.expire session_key, session_timeout
end
private
def redis
Rails.cache.redis
end
def session_timeout
2.weeks.to_i
end
def session_key
'SESSION_' + session.id.to_s if session.id.present?
end
def get_concurrent_session
return {} unless session.id.present?
redis.expire session_key, session_timeout
redis.get(session_key).yield_self do |v|
if v
JSON.parse v, symbolize_names: true
else
{}
end
end
end
end
Usage example:
my_roles = concurrent_session[:my_roles]
update_concurrent_session({my_roles: ['admin', 'vip']})
There really isn't a great solution for this in Rails. My first suggestion would be to examine your use case and see if you can avoid this situation to begin with. If it's safe to do so, session data if often best kept on the client, as there are a number of challenges that can come up when dealing with server-side session stores. On the other hand, if this is data that might be useful long term across multiple page requests (and maybe multiple sessions), perhaps it should go in the database.
Of course, there is some data that really does belong in the session (a good example of this is the currently logged-in user). If this is the case, have a look at http://paulbutcher.com/2007/05/01/race-conditions-in-rails-sessions-and-how-to-fix-them/, and specifically https://github.com/fcheung/smart_session_store, which tries to deal with the situation you've described.
It is a simple race condition, just lock the request using any locking mechanism like
redis locker
RedisLocker.new('my_ajax').run! { session[:whatever] << number }
Load the current session, or create a new one if necessary
Save a copy of the unmodified session for future reference
Run the code of the action
Compare the modified session with the copy saved previously to determine what has changed
If the session has changed:
Lock the session
Reload the session
Apply the changes made to this session and save it
Unlock the session
From Race conditions in Rails sessions and how to fix them.

Rename ActiveResource properties

I am consuming JSON data from a third party API, doing a little bit of processing on that data and then sending the models to the client as JSON. The keys for the incoming data are not named very well. Some of them are acronyms, some just seem to be random characters. For example:
{
aikd: "some value"
lrdf: 1 // I guess this is the ID
}
I am creating a rails ActiveResource model to wrap this resource, but would not like to access these properties through model.lrdf as its not obvious what lrdf really is! Instead, I would like some way to alias these properties to another property that is named better. Something so that I can say model.id = 1 and have that automatically set lrdf to 1 or puts model.id and have that automatically return 1. Also, when I call model.to_json to send the model to the client, I dont want my javascript to have to understand these odd naming conventions.
I tried
alias id lrdf
but that gave me an error saying method lrdf did not exist.
The other option is to just wrap the properties:
def id
lrdf
end
This works, but when I call model.to_json, I see lrdf as the keys again.
Has anyone done anything like this before? What do you recommend?
Have you tried with some before_save magic? Maybe you could define attr_accessible :ldrf, and then, in your before_save filter, assign ldrf to your id field. Haven't tried it, but I think it should works.
attr_accessible :ldrf
before_save :map_attributes
protected
def map_attributes
{:ldrf=>:id}.each do |key, value|
self.send("#{value}=", self.send(key))
end
end
Let me know!
You could try creating a formatter module based on ActiveResource::Formats::JsonFormat and override decode(). If you had to update the data, you'd have to override encode() also. Look at your local gems/activeresource-N.N.N/lib/active_resource/formats/json_format.rb to see what the original json formatter does.
If your model's name is Model and your formatter is CleanupFormatter, just do Model.format = CleanupFormatter.
module CleanupFormatter
include ::ActiveResource::Formats::JsonFormat
extend self
# Set a constant for the mapping.
# I'm pretty sure these should be strings. If not, try symbols.
MAP = [['lrdf', 'id']]
def decode(json)
orig_hash = super
new_hash = {}
MAP.each {|old_name, new_name| new_hash[new_name] = orig_hash.delete(old_name) }
# Comment the next line if you don't want to carry over fields missing from MAP
new_hash.merge!(orig_hash)
new_hash
end
end
This doesn't involve aliasing as you asked, but I think it helps to isolate the gibberish names from your model, which would never have to know those original names existed. And "to_json" will display the readable names.

Resources