Django Multilingual Text Field using JSON - django

I recently ask this question Custom Django MultilingualTextField model field but I found no good reason why I should not do this, so I create a model Field that support multilingual text, auto return text in current language. This basically is the field that store custom Language object to database in json format. Here is the code:
Github: https://github.com/james4388/django-multilingualfield
Ussage:
from django.db import models
from multilingualfield import MLTextField, MLHTMLField
class MyModel(models.Model):
text = MLTextField()
html = MLHTMLField()
Used it like normal text field, translation is auto bases on system language (translation.get_language)
>>>from django.utils import translation
>>>translation.active('en')
>>>m = MyModal.objects.create(text='Hello world',html='<b>Hello world</b>');
>>>m.text
Hello world
>>>translation.active('fr')
>>>m.text #Auto fallback to first language (if any).
Hello world
>>>m.text.value('Bonjour')
>>>m.text.value('Ciao','es')
>>>m.text
Bonjour
>>>m.save()
>>>m.text.get_available_language()
['en', 'fr', 'es']
>>>m.text.remove_language('en')
Field.py
from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models, DatabaseError, transaction
from django.utils.translation import ugettext_lazy as _, get_language
from django.utils import six
try:
import json
except ImportError:
from django.utils import simplejson as json
def get_base_language(lang):
if '-' in lang:
return lang.split('-')[0]
return lang
def get_current_language(base=True):
l = get_language()
if base:
return get_base_language(l)
return l
from .widgets import MultilingualWidget, MultilingualHTMLWidget
from .forms import MultilingualTextFormField, MultilingualHTMLFormField
from .language import LanguageText
class MultilingualTextField(six.with_metaclass(models.SubfieldBase, models.Field)):
"""
A field that support multilingual text for your model
"""
default_error_messages = {
'invalid': _("'%s' is not a valid JSON string.")
}
description = "Multilingual text field"
def __init__(self, *args, **kwargs):
self.lt_max_length = kwargs.pop('max_length',-1)
self.default_language = kwargs.get('default_language', get_current_language())
super(MultilingualTextField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {
'form_class': MultilingualTextFormField,
'widget': MultilingualWidget
}
defaults.update(**kwargs)
return super(MultilingualTextField, self).formfield(**defaults)
def validate(self, value, model_instance):
if not self.null and value is None:
raise ValidationError(self.error_messages['null'])
try:
self.get_prep_value(value)
except:
raise ValidationError(self.error_messages['invalid'] % value)
def get_internal_type(self):
return 'TextField'
def db_type(self, connection):
return 'text'
def to_python(self, value):
if isinstance(value, six.string_types):
if value == "" or value is None:
if self.null:
return None
if self.blank:
return ""
try:
valuejson = json.loads(value)
Lang = LanguageText(max_length=self.lt_max_length,default_language=self.default_language)
Lang.values = valuejson
return Lang
except ValueError:
try:
Lang = LanguageText(value,language=None,max_length=self.lt_max_length,default_language=self.default_language)
return Lang
except:
msg = self.error_messages['invalid'] % value
raise ValidationError(msg)
return value
def get_db_prep_value(self, value, connection=None, prepared=None):
return self.get_prep_value(value)
def get_prep_value(self, value):
if value is None:
if not self.null and self.blank:
return ""
return None
if isinstance(value, six.string_types):
value = LanguageText(value,language=None,max_length=self.lt_max_length,default_language=self.default_language)
if isinstance(value, LanguageText):
value.max_length = self.lt_max_length
value.default_language = self.default_language
return json.dumps(value.values)
return None
def get_prep_lookup(self, lookup_type, value):
if lookup_type in ["exact", "iexact"]:
return self.to_python(self.get_prep_value(value))
if lookup_type == "in":
return [self.to_python(self.get_prep_value(v)) for v in value]
if lookup_type == "isnull":
return value
if lookup_type in ["contains", "icontains"]:
if isinstance(value, (list, tuple)):
raise TypeError("Lookup type %r not supported with argument of %s" % (
lookup_type, type(value).__name__
))
# Need a way co combine the values with '%', but don't escape that.
return self.get_prep_value(value)[1:-1].replace(', ', r'%')
if isinstance(value, dict):
return self.get_prep_value(value)[1:-1]
return self.to_python(self.get_prep_value(value))
raise TypeError('Lookup type %r not supported' % lookup_type)
def value_to_string(self, obj):
return self._get_val_from_obj(obj)
Forms.py
from django import forms
from django.utils import simplejson as json
from .widgets import MultilingualWidget, MultilingualHTMLWidget
from .language import LanguageText
class MultilingualTextFormField(forms.CharField):
widget = MultilingualWidget
def __init__(self, *args, **kwargs):
kwargs['widget'] = MultilingualWidget
super(MultilingualTextFormField, self).__init__(*args, **kwargs)
def clean(self, value):
"""
The default is to have a TextField, and we will decode the string
that comes back from this. However, another use of this field is
to store a list of values, and use these in a MultipleSelect
widget. So, if we have an object that isn't a string, then for now
we will assume that is where it has come from.
"""
value = super(MultilingualTextFormField, self).clean(value)
if not value:
return value
if isinstance(value, basestring):
try:
valuejson = json.loads(value)
Lang = LanguageText()
Lang.values = valuejson
return Lang
except ValueError:
try:
Lang = LanguageText(value,language=None)
return Lang
except:
raise forms.ValidationError(
'JSON decode error: %s' % (unicode(exc),)
)
else:
return value
Language object in language.py
from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models, DatabaseError, transaction
from django.utils.translation import ugettext_lazy as _, get_language
try:
import json
except ImportError:
from django.utils import simplejson as json
def get_base_language(lang):
if '-' in lang:
return lang.split('-')[0]
return lang
def get_current_language(base=True):
l = get_language()
if base:
return get_base_language(l)
return l
class LanguageText(object):
'''
JSON text field blah blah blah
'''
values = {}
default_language = None
max_length = -1
def __init__(self, value=None, language=None, default_language=None, max_length=-1):
self.max_length = max_length
self.default_language = default_language
self.values = {}
if value is not None:
self.value(value,language)
def __call__(self, value=None, language=None):
self.value(value,language)
return self
def get_available_language(self):
return self.values.keys()
def get_current_language(self, base=False):
return get_current_language(base)
def remove_language(self, lang):
try:
return self.values.pop(lang)
except:
pass
def has_language(self, lang):
return self.values.has_key(lang)
def get(self, language=None, fallback=True):
if language is None:
curr_lang = get_current_language(False)
else:
curr_lang = language
curr_lang_base = get_current_language(True)
if curr_lang in self.values:
return self.values[curr_lang]
if not fallback:
return None
if curr_lang_base in self.values:
return self.values[curr_lang_base]
if self.default_language in self.values:
return self.values[self.default_language]
try:
first_lang = self.values.keys()[0]
return self.values[first_lang]
except:
pass
return None
def value(self, value=None, language=None):
if value is None: #Get value
return self.get(language)
else: #Set value
if language is None:
language = get_current_language(False)
if self.max_length != -1:
value = value[:self.max_length]
self.values[language] = value
return None
def __unicode__(self):
return self.value()
def __str__(self):
return unicode(self.value()).encode('utf-8')
def __repr__(self):
return unicode(self.value()).encode('utf-8')
widgets.py
from django import forms
from django.utils import simplejson as json
from django.conf import settings
from .language import LanguageText
from django.template import loader, Context
class MultilingualWidget(forms.Textarea):
def __init__(self, *args, **kwargs):
forms.Widget.__init__(self, *args, **kwargs)
def render(self, name, value, attrs=None):
if value is None: #New create or edit none
vjson = '{}'
aLang = []
Lang = '[]'
Langs = json.dumps(dict(settings.LANGUAGES))
t = loader.get_template('multilingualtextarea.html')
c = Context({"data":value,"vjson":vjson,"lang":Lang,"langs":Langs,"langobjs":settings.LANGUAGES,"fieldname":name})
return t.render(c)
if isinstance(value, LanguageText):
vjson = json.dumps(value.values)
aLang = value.get_available_language()
Lang = json.dumps(aLang)
Langs = json.dumps(dict(settings.LANGUAGES))
t = loader.get_template('multilingualtextarea.html')
c = Context({"data":value,"vjson":vjson,"lang":Lang,"langs":Langs,"langobjs":settings.LANGUAGES,"fieldname":name})
return t.render(c)
return "Invalid data '%s'" % value
So I would like to know is this a good approach? Why shouldn't I do this? Plz help

Code looks good to me.
The only thing that could impact performance is the frequent json encoding/decoding... yet, that shouldn't have a major impact unless you are facing thousands of users on a server with minimal resources.
The previous question you linked to contains some comments noting that adding additional languages might be easier using other means. But in the end - that's a mixture between personal preferences and maintainability. If it fits your project goals, I can't see any reason not to do it the way you've coded it.
Providing proof that your implementation is the best is near to impossible. That is, unless you prove it yourself by creating a different, non-json based implementation and benchmark both on your production server. You'll notice differences will be rather minimal on regular machines. Yet, only the individual numbers will provide actual proof and can help you decide if it's "tuned" and "resource -friendly" enough for your project's purposes. I think it will fit your needs... but that's only my 2 cents.

Related

How to add a key to my DRF API response value

My API returns just the value. I believe it's my fault but am not very versatile with Python/Django. Any help is appreciated.
What is returned
"This is the message"
What I want:
{
"message": "This is a message"
}
All I want to do is to add a word as a key
views.py
from rest_framework.response import Response
from id.models import Id
from rest_framework import generics
from id.serializers import IdSerializer
from django.http import Http404
from IPython import embed
class OfferView(generics.RetrieveAPIView):
serializer_class = IdSerializer
lookup_field = 'id'
def get_queryset(self):
id = self.kwargs['id']
try:
return Id.objects.filter(id=id)
except Mamo.DoesNotExist:
raise Http404
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset, many=True)
try:
if serializer.data[0]['offer_id'] is not None:
result = serializer.data[0]['main_offer']
elif serializer.data[0]['offer_id'] is None:
result = serializer.data[0]['extra_offer']
else:
result = serializer.data[0]['exceptional_offer']
return Response(result)
except IndexError:
raise Http404
Just change
Note: The thing is what you will form that you will get, you were just returning string and that was the problem. Now let's convert that to a dictionary (more specifically the JSON here in REST).
serializer.data is a dictionary. You can form a single dictionary containing the keys & values (JSON serializable) you want. Here I just tried to fulfill the need.
result = serializer.data[0]['exceptional_offer']
to
result = {"message": serializer.data[0]['exceptional_offer']}
and the similar for others as well.
Better way:
Just change the following (last lines, I mean try...except)
try:
if serializer.data[0]['offer_id'] is not None:
result = serializer.data[0]['main_offer']
elif serializer.data[0]['offer_id'] is None:
result = serializer.data[0]['extra_offer']
else:
result = serializer.data[0]['exceptional_offer']
return Response(result)
except IndexError:
raise Http404
to the below one (writing the same piece of code is like code duplication).
try:
if serializer.data[0]['offer_id'] is not None:
# result = serializer.data[0]['main_offer']
key = 'main_offer'
elif serializer.data[0]['offer_id'] is None:
# result = serializer.data[0]['extra_offer']
key = 'extra_offer'
else:
# result = serializer.data[0]['exceptional_offer']
key = 'exceptional_offer'
result = {'message': serializer.data[0][key]}
return Response(result)
except IndexError:
raise Http404
So, I figured out am just returning the result and not creating a JSON Object.
Adding result = {'message': serializer.data[0]…} to all the variable result in the if..else conditions solves it for me.

Python 3 urlopen context manager mocking

I am new to testing and need some help here.
Assuming having this method:
from urllib.request import urlopen
def get_posts():
with urlopen('some url here') as data:
return json.loads(data.read().decode('utf-8'))
The question is how to test this method (using mock.patch decorator if possible)?
What I have now:
#mock.patch('mymodule.urlopen')
def test_get_post(self, mocked_urlopen):
mocked_urlopen.__enter__ = Mock(return_value=self.test_data)
mocked_urlopen.__exit__ = Mock(return_value=False)
...
But it does not seem to be working.
P.S. Is there any convenient way to work with data variable (which type is HTTPResponse) in test so it could just be simple string?
I was fighting with this as well, and finally figured it out. (Python 3 syntax):
import urllib.request
import unittest
from unittest.mock import patch, MagicMock
class TestUrlopen(unittest.TestCase):
#patch('urllib.request.urlopen')
def test_cm(self, mock_urlopen):
cm = MagicMock()
cm.getcode.return_value = 200
cm.read.return_value = 'contents'
cm.__enter__.return_value = cm
mock_urlopen.return_value = cm
with urllib.request.urlopen('http://foo') as response:
self.assertEqual(response.getcode(), 200)
self.assertEqual(response.read(), 'contents')
#patch('urllib.request.urlopen')
def test_no_cm(self, mock_urlopen):
cm = MagicMock()
cm.getcode.return_value = 200
cm.read.return_value = 'contents'
mock_urlopen.return_value = cm
response = urllib.request.urlopen('http://foo')
self.assertEqual(response.getcode(), 200)
self.assertEqual(response.read(), 'contents')
response.close()
here is my take on this
from urllib.request import urlopen
from unittest.mock import patch
class Mock():
def __init__(self, request, context):
return None
def read(self):
return self
def decode(self, arg):
return ''
def __iter__(self):
return self
def __next__(self):
raise StopIteration
with patch('urllib.request.urlopen', Mock):
# do whatever over here
with urlopen('some url here') as data is a context manager
Also, a file can be used as a context manager, so a better approach here is to use io.StringIO
import io
import json
import urllib.request
from unittest.mock import patch
def get_posts():
with urllib.request.urlopen('some url here') as data:
return json.load(data)
def test_get_posts():
data = io.StringIO('{"id": 123}')
with patch.object(urllib.request, 'urlopen', return_value=data):
assert get_posts() == {"id": 123}
Ok, so I have written simple class to simulate context manager.
class PatchContextManager:
def __init__(self, method, enter_return, exit_return=False):
self._patched = patch(method)
self._enter_return = enter_return
self._exit_return = exit_return
def __enter__(self):
res = self._patched.__enter__()
res.context = MagicMock()
res.context.__enter__.return_value = self._enter_return
res.context.__exit__.return_value = self._exit_return
res.return_value = res.context
return res
def __exit__(self, type, value, tb):
return self._patched.__exit__()
Usage:
with PatchContextManager('mymodule.method', 'return_string') as mocked:
a = mymodule.method(47) # a == 'return_string'
mocked.assert_called_with(47)
...

Is there a Django 1.7+ replacement for South's add_introspection_rules()?

Back in the days of South migrations, if you wanted to create a custom model field that extended a Django field's functionality, you could tell South to use the introspection rules of the parent class like so:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^myapp\.stuff\.fields\.SomeNewField"])
Now that migrations have been moved to Django, is there a non-South equivalent of the above? Is an equivalent even needed anymore, or is the new migration stuff smart enough to just figure it out on its own?
As Phillip mentions in the comments, deconstruct() is the official way to handle custom fields in django migrations.
To go on to complete the request for clarification... It would appear that there are already a couple of examples of code out there written to handle both. For example, this excerpt (to handle the on parameter for ExclusiveBooleanField) is taken from django-exclusivebooleanfield:
from django.db import models, transaction
from django.db.models import Q
from six import string_types
from six.moves import reduce
try:
transaction_context = transaction.atomic
except AttributeError:
transaction_context = transaction.commit_on_success
class ExclusiveBooleanField(models.BooleanField):
"""
Usage:
class MyModel(models.Model):
the_one = ExclusiveBooleanField()
class MyModel(models.Model):
field_1 = ForeignKey()
field_2 = CharField()
the_one = ExclusiveBooleanField(on=('field_1', 'field_2'))
# `on` is a bit like a unique constraint, value of field
# is only exclusive for rows with same value of the on fields
"""
def __init__(self, on=None, *args, **kwargs):
if isinstance(on, string_types):
on = (on, )
self._on_fields = on or ()
super(ExclusiveBooleanField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
super(ExclusiveBooleanField, self).contribute_to_class(cls, name)
models.signals.class_prepared.connect(self._replace_save, sender=cls)
def deconstruct(self):
"""
to support Django 1.7 migrations, see also the add_introspection_rules
section at bottom of this file for South + earlier Django versions
"""
name, path, args, kwargs = super(
ExclusiveBooleanField, self).deconstruct()
if self._on_fields:
kwargs['on'] = self._on_fields
return name, path, args, kwargs
def _replace_save(self, sender, **kwargs):
old_save = sender.save
field_name = self.name
on_fields = self._on_fields
def new_save(self, *args, **kwargs):
def reducer(left, right):
return left & Q(**{right: getattr(self, right)})
with transaction_context():
if getattr(self, field_name) is True:
f_args = reduce(reducer, on_fields, Q())
u_args = {field_name: False}
sender._default_manager.filter(f_args).update(**u_args)
old_save(self, *args, **kwargs)
new_save.alters_data = True
sender.save = new_save
try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules(
rules=[
(
(ExclusiveBooleanField,),
[],
{"on": ["_on_fields", {"default": tuple()}]},
)
],
patterns=[
'exclusivebooleanfield\.fields\.ExclusiveBooleanField',
]
)
except ImportError:
pass

Django rest framework add field in serializer for using with post method only

am trying to build a serializer using HyperlinkedModelSerializer, yet I have a scenario where I want to add a field which does not exist in the model but I will require the value for validating the transaction, I found the below snippet
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework import serializers
import logging
# initiate logger
logging.getLogger(__name__)
class PostHyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
"""
Options for PostHyperlinkedModelSerializer
"""
def __init__(self, meta):
super(PostHyperlinkedModelSerializerOptions, self).__init__(meta)
self.postonly_fields = getattr(meta, 'postonly_fields', ())
class PostHyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
_options_class = PostHyperlinkedModelSerializerOptions
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = {}
for field_name, field in self.fields.items():
# Ignore all postonly_fields fron serialization
if field_name in self.opts.postonly_fields:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
ret.fields[key] = field
return ret
def restore_object(self, attrs, instance=None):
model_attrs, post_attrs = {}, {}
for attr, value in attrs.iteritems():
if attr in self.opts.postonly_fields:
post_attrs[attr] = value
else:
model_attrs[attr] = value
obj = super(PostHyperlinkedModelSerializer,
self).restore_object(model_attrs, instance)
# Method to process ignored postonly_fields
self.process_postonly_fields(obj, post_attrs)
return obj
def process_postonly_fields(self, obj, post_attrs):
"""
Placeholder method for processing data sent in POST.
"""
pass
class PurchaseSerializer(PostHyperlinkedModelSerializer):
""" PurchaseSerializer
"""
currency = serializers.Field(source='currency_used')
class Meta:
model = DiwanyaProduct
postonly_fields = ['currency', ]
Am using the above class, PostHyperlinkedModelSerializer, but for some reason the above is causing a problem with the browsable api interface for rest framework. Field labels are disappearing, plus the new field "currency" is not showing in the form (see screenshot below for reference). Any one can help on that?
Whoever wrote the code probably didn't need browsable api (normal requests will work fine).
In order to fix the api change to_native to this:
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = self._dict_class()
for field_name, field in self.fields.items():
if field.read_only and obj is None:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
if field_name not in self.opts.postonly_fields:
ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value)
return ret

Django custom widget to split an IntegerField into hours and seconds fields in a Model Form

I am storing time duration as minutes in an IntegerField. In my front end form (Model Form), I want to split the input into one field for minutes and one field for hours. I also want to be able to use the InLineFormset. Can this be done in a nice way? I could use javascript, but that does not seeml like a good solution to me, or is it?
You want to use a MultiWidget. I'd suggest that you override the field itself. Have a look at the docs for creating custom fields.
If you can, it might be worth storing your data as a floating point representing a python timedelta. You may want to add a few custom methods to your field. Using timedelta will be useful if you want to do any calculations with your field.
You'll probably want to do a bit of reading on form validation too.
I've tested the following hours, mins, sec multiwidget with float based custom field on Django 1.4 -
from datetime import timedelta
from django import forms
from django.db import models
from django.core import exceptions, validators
def validate_timedelta(value):
if not isinstance(value, timedelta):
raise exceptions.ValidationError('A valid time period is required')
class SplitHoursMinsWidget(forms.widgets.MultiWidget):
def __init__(self, *args, **kwargs):
widgets = (
forms.TextInput(),
forms.TextInput(),
forms.TextInput(),
)
super(SplitHoursMinsWidget, self).__init__(widgets, *args, **kwargs)
def decompress(self, value):
if value:
return [value.seconds // 3600, (value.seconds // 60) % 60,
value.seconds % 60]
return [None, None, None]
def format_output(self, rendered_widgets):
return ("HH:MM:SS " + rendered_widgets[0] + ":" + rendered_widgets[1] +
":" + rendered_widgets[2])
def value_from_datadict(self, data, files, name):
hours_mins_secs = [
widget.value_from_datadict(data, files, name + '_%s' % i)
for i, widget in enumerate(self.widgets)]
try:
time_delta = (timedelta(hours=float(hours_mins_secs[0])) +
timedelta(minutes=float(hours_mins_secs[1])) +
timedelta(seconds=float(hours_mins_secs[2])))
except ValueError:
return None
else:
return time_delta
class TimeDeltaFormField(forms.Field):
widget = SplitHoursMinsWidget
class TimeDeltaField(models.Field):
__metaclass__ = models.SubfieldBase
description = "Field to hold a python timedelta object"
default_validators = [validate_timedelta, ]
def to_python(self, value):
if value in validators.EMPTY_VALUES or isinstance(value, timedelta):
return value
try:
return timedelta(seconds=float(value))
except:
raise exceptions.ValidationError('A valid time period is required')
def get_prep_value(self, value):
return float(value.days * 86400 + value.seconds)
def get_internal_type(self):
return 'FloatField'
def formfield(self, **kwargs):
defaults = {'form_class': TimeDeltaFormField}
defaults.update(kwargs)
return super(TimeDeltaField, self).formfield(**defaults)
It's worth noting that this doesn't provide any useful input level validation (it's possible to enter over 60 in both the minutes and seconds input elements). Perhaps that could be best resolved with javascript.