Altering model after field validation - python

I'm implementing a tagging system. Currently, the models look like this:
class Tag(models.Model):
label = models.CharField(max_length=MAX_TAG_LENGTH)
class TagManager(models.Model):
tags = models.ManyToManyField(Tag, related_name="referrers")
def associate_tag(self, tag_label: str):
. . .
And I have a custom field that cuts its input on commas so the user can enter tags as a comma-separated list:
class TagsField(forms.CharField):
def to_python(self, value):
if not value:
return []
return [tag.strip() for tag in value.split(',')]
Finally, I have the model and form where these are used:
class Thing(models.Model):
tags = models.OneToOneField(TagManager, on_delete=models.SET_NULL, null=True)
class ThingForm(forms.ModelForm):
tags = TagsField(widget=forms.TextInput(attrs={"placeholder": "Tags", "required": False}))
class Meta:
model = Thing
fields = ["tags"]
Problem
My issue is that if I populate and validate the form:
form = ThingForm(data={"tags": ["One", "Two"]})
form.is_valid()
I get errors:
{'tags': ["“["One", "Two"]” value must be an integer."]}
Which I'm guessing is because it's trying to place the stringified list in the OneToOneField, which isn't going to work.
What I really need to do is after validating the field, I need to iterate the results of to_python, and call thing_instance.tags.associate_tag on each of the validated tag strings.
Is there a "hook" method on forms that will allow me to do this cleanly? I've read over the docs and Form source and can't find any obvious candidates.

I realized as I was writing this that it's a clean_* method that I need. This didn't strike me as "cleaning" on first brush, so I ignored it.
The solution was to add a clean_tags method to the ThingForm class:
def clean_tags(self):
tags = self.cleaned_data["tags"]
for tag in tags:
self.instance.tags.associate_tag(tag)
return self.instance.tags.pk
It associates the cleaned tags, and then returns the PK of the TagManager they were added to.

Related

In Django, how to annotate a queryset with the string representation each object?

I'm working on a view which inherits from Django's ListView and uses a search form through a SearchMixin. Here is the (simplified) view:
class ListBase(DashboardAccessMixin, SearchMixin):
queryset = CheckInType.objects.all()
def get_queryset(self):
queryset = super().get_queryset()
query = self.search_data('q')
if query:
queryset = queryset.filter(
Q(title__icontains=query) | Q(description__icontains=query))
return queryset
class CheckInTypeList(ListBase, ListView):
template_name = 'check_in_types/index.html'
The template, however, does not actually show the {{ object.title }}, but shows just {{ object }}, which corresponds to the string representation of the CheckInType. This is defined as follows:
class CheckInType(TimeStampedModel, TimingMixIn):
title = models.CharField(blank=True, max_length=255)
description = models.TextField(blank=True)
# Timing
is_prenatal = models.BooleanField(default=False)
min_weeks = models.IntegerField(default=0)
max_weeks = models.IntegerField(default=0)
def __str__(self):
if self.title:
return self.title
else:
prenatal = 'Prenatal' if self.is_prenatal else 'Postpartum'
return f'{prenatal} Check-In: {self.timing}'
I'll omit the details of the TimingMixin for brevity, but self.timing is basically a string like "0-4 weeks" composed from the min_weeks and max_weeks fields.
The result of this, however, is that the filtering does not exactly correspond to what the user sees. Consider this example:
Here, the first entry in the list entitled "Postpartum Check-In: 0 weeks" is an automatically generated 'title'. So if I search for "Postpartum" in the search bar and press Enter, I get only one result:
whereas I would expect two results. (Incidentally, there is also an OrderingMixin to sort the results alphabetically by title, which is also not working for the same reason).
In order to fix this, I would like to add a field 'str' to the queryset which contains the string representation of the object.
Following the comment in How can i get the string representation from queryset in django, I looked into annotate, but I wasn't able to find exactly what I was looking for. I would like something like
queryset = queryset.annotate(str=str(object))
so that after that I could add Q(str__icontains=query) to the filter. I'm not sure whether this is possible, however, because the aggregation functions like Count() are general database functions which don't really 'know about' Python's string representation of the model.
How can I achieve filtering the results by their actual string representation? (I would preferably like to do this by annotating the queryset because that would work for the OrderingMixin as well).

Django Rest Framework - Read nested data, write integer

So far I'm extremely happy with Django Rest Framework, which is why I alsmost can't believe there's such a large omission in the codebase. Hopefully someone knows of a way how to support this:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True, source='item')
item = serializers.IntegerSerializer(write_only=True)
class Meta:
model = Pin
with the goal
The goal here is to read:
{pin: item: {name: 'a', url: 'b'}}
but to write using an id
{pin: item: 10}
An alternative would be to use two serializers, but that looks like a really ugly solution:
django rest framework model serializers - read nested, write flat
Django lets you access the Item on your Pin with the item attribute, but actually stores the relationship as item_id. You can use this strategy in your serializer to get around the fact that a Python object cannot have two attributes with the same name (a problem you would encounter in your code).
The best way to do this is to use a PrimaryKeyRelatedField with a source argument. This will ensure proper validation gets done, converting "item_id": <id> to "item": <instance> during field validation (immediately before the serializer's validate call). This allows you to manipulate the full object during validate, create, and update methods. Your final code would be:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True)
item_id = serializers.PrimaryKeyRelatedField(write_only=True,
source='item',
queryset=Item.objects.all())
class Meta:
model = Pin
fields = ('id', 'item', 'item_id',)
Note 1: I also removed source='item' on the read-field as that was redundant.
Note 2: I actually find it rather unintuitive that Django Rest is set up such that a Pin serializer without an Item serializer specified returns the item_id as "item": <id> and not "item_id": <id>, but that is beside the point.
This method can even be used with forward and reverse "Many" relationships. For example, you can use an array of pin_ids to set all the Pins on an Item with the following code:
class ItemSerializer(serializers.ModelSerializer):
pins = PinSerializer(many=True, read_only=True)
pin_ids = serializers.PrimaryKeyRelatedField(many=True,
write_only=True,
source='pins',
queryset=Pin.objects.all())
class Meta:
model = Item
fields = ('id', 'pins', 'pin_ids',)
Another strategy that I previously recommended is to use an IntegerField to directly set the item_id. Assuming you are using a OneToOneField or ForeignKey to relate your Pin to your Item, you can set item_id to an integer without using the item field at all. This weakens the validation and can result in DB-level errors from constraints being violated. If you want to skip the validation DB call, have a specific need for the ID instead of the object in your validate/create/update code, or need simultaneously writable fields with the same source, this may be better, but I wouldn't recommend anymore. The full line would be:
item_id = serializers.IntegerField(write_only=True)
If you are using DRF 3.0 you can implement the new to_internal_value method to override the item field to change it to a PrimaryKeyRelatedField to allow the flat writes. The to_internal_value takes unvalidated incoming data as input and should return the validated data that will be made available as serializer.validated_data. See the docs: http://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data
So in your case it would be:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
class PinSerializer(ModelSerializer):
item = ItemSerializer()
# override the nested item field to PrimareKeyRelatedField on writes
def to_internal_value(self, data):
self.fields['item'] = serializers.PrimaryKeyRelatedField(queryset=Item.objects.all())
return super(PinSerializer, self).to_internal_value(data)
class Meta:
model = Pin
Two things to note: The browsable web api will still think that writes will be nested. I'm not sure how to fix that but I only using the web interface for debug so not a big deal. Also, after you write the item returned will have flat item instead of the nested one. To fix that you can add this code to force the reads to use the Item serializer always.
def to_representation(self, obj):
self.fields['item'] = ItemSerializer()
return super(PinSerializer, self).to_representation(obj)
I got the idea from this from Anton Dmitrievsky's answer here: DRF: Simple foreign key assignment with nested serializers?
You can create a Customized Serializer Field (http://www.django-rest-framework.org/api-guide/fields)
The example took from the link:
class ColourField(serializers.WritableField):
"""
Color objects are serialized into "rgb(#, #, #)" notation.
"""
def to_native(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
def from_native(self, data):
data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue)
Then use this field in your serializer class.
I create a Field type that tries to solve the problem of the Data Save requests with its ForeignKey in Integer, and the requests to read data with nested data
This is the class:
class NestedRelatedField(serializers.PrimaryKeyRelatedField):
"""
Model identical to PrimaryKeyRelatedField but its
representation will be nested and its input will
be a primary key.
"""
def __init__(self, **kwargs):
self.pk_field = kwargs.pop('pk_field', None)
self.model = kwargs.pop('model', None)
self.serializer_class = kwargs.pop('serializer_class', None)
super().__init__(**kwargs)
def to_representation(self, data):
pk = super(NestedRelatedField, self).to_representation(data)
try:
return self.serializer_class(self.model.objects.get(pk=pk)).data
except self.model.DoesNotExist:
return None
def to_internal_value(self, data):
return serializers.PrimaryKeyRelatedField.to_internal_value(self, data)
And so it would be used:
class PostModelSerializer(serializers.ModelSerializer):
message = NestedRelatedField(
queryset=MessagePrefix.objects.all(),
model=MessagePrefix,
serializer_class=MessagePrefixModelSerializer
)
I hope this helps you.

Override django-taggit tags so that they are always lowercase

I couldnt find a good answer or solution for multiple tags in a model. the only thing I found close was this:
How can I limit django-taggit to accept only lowercase words?
here is my current code:
from taggit.managers import TaggableManager
from taggit.models import TaggedItemBase
class TaggedStory(TaggedItemBase):
content_object = models.ForeignKey("Story")
class TaggedSEO(TaggedItemBase):
content_object = models.ForeignKey("Story")
class Story(models.Model):
...
tags = TaggableManager(through=TaggedStory, blank=True, related_name='story_tags')
...
seo_tags = TaggableManager(through=TaggedSEO, blank=True, related_name='seo_tags')
I usually implement this at the form level:
def clean_tags(self):
"""
Force all tags to lowercase.
"""
tags = self.cleaned_data.get('tags', None)
if tags:
tags = [t.lower() for t in tags]
return tags
It really depends on how you look at it. I'm happy with solution because I consider it a validation problem. If you consider it a data integrity problem, I can understand why you'd want to do it at the model level. At which point your best bet is to subclass the taggit modules to a point that you can override Tag.save().
In the appname --> utils file, for eg:(blog --> init.py), define a function as follows:
def comma_splitter(tag_string):
"""convert each tag into lowercase"""
return [t.strip().lower() for t in tag_string.split(',') if t.strip()]
And then, in the settings.py file, define and override the default taggit settings as:
TAGGIT_TAGS_FROM_STRING = 'blog.init.comma_splitter'

Returning extended fields in JSON

I have two tabels(Ingredient_Step and Ingredient) in on relation as you can see below:
Models.Py
class Ingredient_Step(models.Model):
ingredient = models.ForeignKey(Ingredient)
Step = models.ForeignKey(Step)
def __unicode__(self):
return u'{}'.format(self.Step)
class Ingredient(models.Model):
IngredientName = models.CharField(max_length=200,unique=True)
Picture = models.ImageField(upload_to='Ingredient')
def __unicode__(self):
return u'{}'.format(self.IngredientName)
In a function, i need serialize a JSON object from a query that returns from "Ingredient_step", but I need send the field "IngredientName", who comes from "Ingredient" table.
I try using "ingredient__IngredientName" but it fails.
Views.Py:
def IngredientByStep(request):
if request.is_ajax() and request.GET and 'id_Step' in request.GET:
if request.GET["id_Step"] != '':
IngStp = Ingredient_Step.objects.filter(Step =request.GET["id_Step"])
return JSONResponse(serializers.serialize('json', IngStp, fields=('pk','ingredient__IngredientName')))
How i can call extends field from a relation?
Thanks
This "feature" of Django (and many ORM's like SQLAlchemy) are called Lazy Loading, meaning data is only loaded from related models if you specifically ask for them. In this case, build your IngStp as a list of results, and make sure to access the property for each result before serializing.
Here's an example of how to do that: Django: Include related models in JSON string?

Can model views in Flask-Admin hyperlink to other model views?

Let's suppose we have a model, Foo, that references another model, User - and there are Flask-Admin's ModelView for both.
On the Foo admin view page
I would like the entries in the User column to be linked to the corresponding User model view.
Do I need to modify one of Flask-Admin's templates to achieve this?
(This is possible in the Django admin interface by simply outputting HTML for a given field and setting allow_tags (ref) True to bypass Django's HTML tag filter)
Some example code based on Joes' answer:
class MyFooView(ModelView):
def _user_formatter(view, context, model, name):
return Markup(
u"<a href='%s'>%s</a>" % (
url_for('user.edit_view', id=model.user.id),
model.user
)
) if model.user else u""
column_formatters = {
'user': _user_formatter
}
Use column_formatters for this: https://flask-admin.readthedocs.org/en/latest/api/mod_model/#flask.ext.admin.model.BaseModelView.column_formatters
Idea is pretty simple: for a field that you want to display as hyperlink, either generate a HTML string and wrap it with Jinja2 Markup class (so it won't be escaped in templates) or use macro helper: https://github.com/mrjoes/flask-admin/blob/master/flask_admin/model/template.py
Macro helper allows you to use custom Jinja2 macros in overridden template, which moves presentational logic to templates.
As far as URL is concerned, all you need is to find endpoint name generated (or provided) for the User model and do url_for('userview.edit_view', id=model.id) to generate the link.
extra information for #wodow, notice that model.user is wrong if you use pymongo as the backend, because the model in pymongo is a dict type, you can just use model['name'] to replace it
Adding this code to each of your models that have referenced by other models and flask-admin and jinja will take care of the name you want to display on the screen, just replace that with whatever you prefer:
def __unicode__(self):
return self.name # or self.id or whatever you prefer
for example:
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
def __unicode__(self):
return self.name
class MasterUser(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])

Categories