I'm facing problems using get_or_create() in my view.
What I want to do is have the User get or create an instance of the Keyword model whenever he wants to add a keyword.
I have a Keyword model that looks like this:
class Keyword(models.Model):
word = models.CharField(max_length=30, unique=True, default=None)
members = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, default=None)
def __str__(self):
return self.keywords
I have a form to create the keyword:
class KeywordForm(forms.ModelForm):
keywords = forms.CharField(max_length=30)
def __init__(self, *args, **kwargs):
super(KeywordForm, self).__init__(*args, **kwargs)
self.fields["keywords"].unique = False
class Meta:
fields = ("keywords",)
model = models.Keyword
I've tried different things in the view and here is my current version, without the use of get_or_create. It only creates the keyword:
class KeywordCreationView(LoginRequiredMixin, generic.CreateView):
form_class = forms.KeywordForm
model = models.Keyword
page_title = 'Add a new keyword'
success_url = reverse_lazy("home")
template_name = "accounts/add_keyword.html"
def form_valid(self, form):
var = super(KeywordCreationView, self).form_valid(form)
self.object.user = self.request.user
self.object.save()
self.object.members.add(self.object.user)
return var
How should my view look in order to get the keyword if it exists and if it does, add the User as 'member'. If it doesn't, create the Keyword.
Thanks for your help!
I do believe CreateView isn't the right class for this. You should use UpdateView instead and override the get_object method (which is actually a part of the SingleObjectMixin) ancestor of this class based view.
The source code of this mixin is rather daunging but in your case something as simple as
def get_object(self, queryset=None):
pk = self.kwargs.get(self.pk_url_kwarg)
if queryset:
obj, c = queryset.get_or_create(pk=pk)
else:
obj, c = MyModel.get_or_create(pk=pk)
return obj
might work. But frankly, it's alot simpler to use a simple (non class based view)
Related
I am working on a Django project with two models linked by a ForeignKey. The parent model, Composition, is linked to the child model, NoteObject, by the id of Composition.
in models.py
class Composition(models.Model):
id = models.AutoField(primary_key=True)
...
class NoteObject(models.Model):
id = models.AutoField(primary_key=True)
composition = models.ForeignKey(Composition, on_delete=models.CASCADE)
...
Once a composition is created, the user needs to be able to create NoteObjects that belong to that composition. The notes are created with the following method:
in views.py
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def get_context_data(self, **kwargs):
kwargs['notes'] = NoteObject.objects.filter(
composition=self.kwargs['composition'])
return super(NoteCreateView, self).get_context_data(**kwargs)
The get_context_data method is there to display only the notes for the current composition. The current composition comes from the id of the composition that is part of the url where <composition> is the id of the composition.
in urls.py
path('entry/<composition>/', views.NoteCreateView.as_view(), name='entry')
When I save a NoteObject, what do I need to do in order to set the value of the ForeignKey to be the value within <composition>?
in models.py
def save(self, *args, **kwargs):
composition_id = ????????
self.composition_id = composition_id
super(NoteObject, self).save(*args, **kwargs)
How do I get the value of kwarg in the CreateView to be the ForeignKey when the object is created?
I think you can do it by over-riding form_valid method as mentioned in docs here.
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def form_valid(self, form):
form.instance.composition = self.kwargs['composition']
return super(NoteCreateView, self).form_valid(form)
In order to make this work, you need to override the dispatch function as well.
def dispatch(self, request, *args, **kwargs):
self.composition = Composition.objects.values_list(
'id').filter(pk=kwargs['composition'])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.composition_id = self.composition
return super().form_valid(form)
I used this solution and modified it slightly: CreateView Set ForeignKey from Url Parameter.
I'm using the CreateView of Django and I'm trying to find out how I can modify any text which gets sent before it gets saved. For example, right now I'm only looking to lowercase all the text before saving.
I know I need to use form_valid() but I can't seem to get it right.
forms.py
class ConfigForm(forms.ModelForm):
class Meta:
model = Config
fields = ["heading", "name", "data", "rating"]
views.py
def form_valid(self, form):
super().form_valid(form)
form.fields["heading"].lower()
form.fields["name"].lower()
form.fields["data"].lower()
That shouldn't be done in form_valid. You should do that in the form itself. Instead of letting CreateView automatically create a form for you, do it explicitly and overwrite the clean method.
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('list', 'of', 'fields')
def clean(self):
for field, value in self.cleaned_data.items():
self.cleaned_data['field'] = value.lower()
...
class MyCreateView(views.CreateView):
form_class = MyForm
Override get_form_kwargs method to update the kwargs which instantiates the form.
Solution:
def get_form_kwargs(self):
# update super call if python < 3
form_kwargs = super().get_form_kwargs()
form_kwargs['data']['str_field_name'] = form_kwargs['data']['str_field_name'].lower()
return form_kwargs
Ref:
get_form_kwargs docs
While it may not be the nicest solution, it can be done like this:
def form_valid(self, form):
self.object = form.save(commit=False)
# ...
self.object.save()
return http.HttpResponseRedirect(self.get_success_url())
Just for the record
In the first case
def get_form_kwargs(self):
# update super call if python < 3
form_kwargs = super().get_form_kwargs()
form_kwargs['data']['str_field_name'] = form_kwargs['data'['str_field_name'].lower()
return form_kwargs
Django complains "This QueryDict instance is immutable". And workaround is
data = kwargs['data'].copy() # mutable copy
data['foo'] = 'whatever' #supply the missing default value
kwargs['data'] = data
I want to hide specific fields of a model on the list display at persons/ and show all the fields on the detail display persons/jane
I am relatively new to the rest framework and the documentation feels like so hard to grasp.
Here's what I am trying to accomplish.
I have a simple Person model,
# model
class Person(models.Model):
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
nickname = models.CharField(max_length=20)
slug = models.SlugField()
address = models.TextField(max_length=300, blank=True)
and the serializer class
# serializers
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('nickname', 'slug')
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
and the viewsets.
# view sets (api.py)
class PersonListViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonListSerializer
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
at the url persons I want to dispaly list of persons, just with fields nickname and slug and at the url persons/[slug] I want to display all the fields of the model.
my router configurations,
router = routers.DefaultRouter()
router.register(r'persons', api.PersonListViewSet)
router.register(r'persons/{slug}', api.PersonViewSet)
I guess the second configuration is wrong, How can I achieve what I am trying to do?
update:
the output to persons/slug is {"detail":"Not found."} but it works for person/pk
Thank you
For anyone else stumbling across this, I found overriding get_serializer_class on the viewset and defining a serializer per action was the DRY-est option (keeping a single viewset but allowing for dynamic serializer choice):
class MyViewset(viewsets.ModelViewSet):
serializer_class = serializers.ListSerializer
permission_classes = [permissions.IsAdminUser]
renderer_classes = (renderers.AdminRenderer,)
queryset = models.MyModel.objects.all().order_by('-updated')
def __init__(self, *args, **kwargs):
super(MyViewset, self).__init__(*args, **kwargs)
self.serializer_action_classes = {
'list':serializers.AdminListSerializer,
'create':serializers.AdminCreateSerializer,
'retrieve':serializers.AdminRetrieveSerializer,
'update':serializers.AdminUpdateSerializer,
'partial_update':serializers.AdminUpdateSerializer,
'destroy':serializers.AdminRetrieveSerializer,
}
def get_serializer_class(self, *args, **kwargs):
"""Instantiate the list of serializers per action from class attribute (must be defined)."""
kwargs['partial'] = True
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(MyViewset, self).get_serializer_class()
Hope this helps someone else.
You can override the 'get_fields' method your serializer class and to add something like that:
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
request = self.context.get('request')
if request is not None and not request.parser_context.get('kwargs'):
fields.pop('your_field', None)
return fields
In this case when you get detail-view there is 'kwargs': {'pk': 404} and when you get list-view there is 'kwargs': {}
I wrote an extension called drf-action-serializer (pypi) that adds a serializer called ModelActionSerializer that allows you to define fields/exclude/extra_kwargs on a per-action basis (while still having the normal fields/exclude/extra_kwargs to fall back on).
The implementation is nice because you don't have to override your ViewSet get_serializer method because you're only using a single serializer. The relevant change is that in the get_fields and get_extra_kwargs methods of the serializer, it inspects the view action and if that action is present in the Meta.action_fields dictionary, then it uses that configuration rather than the Meta.fields property.
In your example, you would do this:
from action_serializer import ModelActionSerializer
class PersonSerializer(ModelActionSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
action_fields = {
'list': {'fields': ('nickname', 'slug')}
}
Your ViewSet would look something like:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
And your router would look normal, too:
router = routers.DefaultRouter()
router.register(r'persons', api.PersonViewSet)
Implementation
If you're curious how I implemented this:
I added a helper method called get_action_config which gets the current view action and returns that entry in the action_fields dict:
def get_action_config(self):
"""
Return the configuration in the `Meta.action_fields` dictionary for this
view's action.
"""
view = getattr(self, 'context', {}).get('view', None)
action = getattr(view, 'action', None)
action_fields = getattr(self.Meta, 'action_fields', {})
I changed get_field_names of ModelSerializer:
From:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
To:
action_config = self.get_action_config()
if action_config:
fields = action_config.get('fields', None)
exclude = action_config.get('exclude', None)
else:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
Finally, I changed the get_extra_kwargs method:
From:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
To:
action_config = self.get_action_config()
if action_config:
extra_kwargs = copy.deepcopy(action_config.get('extra_kwargs', {}))
else:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
If you want to change what fields are displayed in the List vs Detail view, the only thing you can do is change the Serializer used. There's no field that I know of that lets you specify which fields of the Serializer gets used.
The field selection on you serializers should be working, but I don't know what might be happening exactly. I have two solutions you can try:
1 Try to change the way you declare you serializer object
#If you aren't using Response:
from rest_framework.response import Response
class PersonListViewSet(viewsets.ModelViewSet):
def get(self, request):
queryset = Person.objects.all()
serializer_class = PersonListSerializer(queryset, many=True) #It may change the things
return Response(serializer_class.data)
class PersonViewSet(viewsets.ModelViewSet):
def get(self, request, pk): #specify the method is cool
queryset = Person.objects.all()
serializer_class = PersonSerializer(queryset, many=True) #Here as well
#return Response(serializer_class.data)
2 The second way around would change your serializers
This is not the most normal way, since the field selector should be working but you can try:
class PersonListSerializer(serializers.ModelSerializer):
nickname = serializers.SerializerMethodField() #Will get the attribute my the var name
slug = serializers.SerializerMethodField()
class Meta:
model = Person
def get_nickname(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.nickname
def get_slug(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.slug
I hope it helps. Try to see the APIview class for building your view too.
Somehow close:
If you just want to skip fields in the serilaizer
class UserSerializer(serializers.ModelSerializer):
user_messages = serializers.SerializerMethodField()
def get_user_messages(self, obj):
if self.context.get('request').user != obj:
# do somthing here check any value from the request:
# skip others msg
return
# continue with your code
return SystemMessageController.objects.filter(user=obj, read=False)
I rewrite ModelViewSet list function to modify serializer_class.Meta.fields attribute, code like this:
class ArticleBaseViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
exclude = ["content"]
self.serializer_class.Meta.fields = [f.name for f in self.serializer_class.Meta.model._meta.fields if f.name not in exclude]
queryset = self.filter_queryset(self.get_queryset()).filter(is_show=True, is_check=True)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class BannerArticleViewSet(ArticleBaseViewSet):
queryset = BannerArticle.objects.filter(is_show=True, is_check=True).all()
serializer_class = BannerArticleSerializer
permission_classes = (permissions.AllowAny,)
But it looks not stable, so i will not use it, just share to figure out the best way
My solution.
class BaseSerializerMixin(_ModelSerializer):
class Meta:
exclude: tuple[str, ...] = ()
exclude_in_list: tuple[str, ...] = ()
model: Type[_models.Model]
def get_action(self) -> Optional[str]:
if 'request' not in self.context:
return None
return self.context['request'].parser_context['view'].action
def get_fields(self):
fields = super().get_fields()
if self.get_action() == 'list':
[fields.pop(i) for i in list(fields) if i in self.Meta.exclude_in_list]
return fields
I think it should be like this:
router.register(r'persons/?P<slug>/', api.PersonViewSet)
and you should include a line like this:
lookup_field='slug'
in your serializer class. Like this:
class PersonSerializer(serializers.ModelSerializer):
lookup_field='slug'
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
I'm working on a project with some social features and need to make it so that a User can see all details of his profile, but only public parts of others' profiles.
Is there a way to do this within one ViewSet?
Here's a sample of my model:
class Profile(TimestampedModel):
user = models.OneToOneField(User)
nickname = models.CharField(max_length=255)
sex = models.CharField(
max_length=1, default='M',
choices=(('M', 'Male'), ('F', 'Female')))
birthday = models.DateField(blank=True, null=True)
For this model, I'd like the birthday, for example, to stay private.
In the actual model there's about a dozen such fields.
My serializers:
class FullProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
class BasicProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = read_only_fields = ('nickname', 'sex', 'birthday')
A custom permission I wrote:
class ProfilePermission(permissions.BasePermission):
"""
Handles permissions for users. The basic rules are
- owner and staff may do anything
- others can only GET
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return request.user == obj.user or request.user.is_staff
And my viewset:
class RUViewSet(
mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet with update/retrieve powers."""
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
What I'd like is for request.user's own profile in the queryset to be serialized using FullProfileSerializer, but the rest using BasicProfileSerializer.
Is this at all possible using DRF's API?
We can override the retrieve() and list methods in our ProfileViewSet to return different serialized data depending on the user being viewed.
In the list method, we serialize all the user instances excluding the current user with the serializer returned from get_serializer_class() method. Then we serialize the current user profile information using the FullProfileSerializer explicitly and add this serialized data to the data returned before.
In the retrieve method, we set a accessed_profile attribute on the view to know about the user the view is displaying. Then, we will use this attribute to decide the serializer in the get_serializer_class() method.
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def list(self, request, *args, **kwargs):
instance = self.filter_queryset(self.get_queryset()).exclude(user=self.request.user)
page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
serializer = self.get_serializer(instance, many=True)
other_profiles_data = serializer.data # serialized profiles data for users other than current user
current_user_profile = <get_the_current_user_profile_object>
current_user_profile_data = FullProfileSerializer(current_user_profile).data
all_profiles_data = other_profiles_data.append(current_user_profile_data)
return Response(all_profiles_data)
def retrieve(self, request, *args, **kwargs):
self.accessed_profile = self.get_object() # set this as on attribute on the view
serializer = self.get_serializer(self.accessed_profile)
return Response(serializer.data)
def get_serializer_class(self):
current_user = self.request.user
if current_user.is_staff or (self.action=='retrieve' and self.accessed_profile.user==current_user):
return FullProfileSerializer
return BasicProfileSerializer
I managed to hack together the solution that provides the wanted behaviour for the detail view:
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
def get_serializer(self, instance=None, *args, **kwargs):
if hasattr(instance, 'user'):
user = self.request.user
if instance.user == user or user.is_staff:
kwargs['instance'] = instance
kwargs['context'] = self.get_serializer_context()
return FullProfileSerializer(*args, **kwargs)
return super(ProfileViewSet, self).get_serializer(
instance, *args, **kwargs)
This doesn't work for the list view, however, as that one provides the get_serializer method with a Django Queryset object in place of an actual instance.
I'd still like to see this behaviour in a list view, i.e. when serializing many objects, so if anyone knows a more elegant way to do this that also covers the list view I'd much appreciate your answer.
I am trying to log the activities during save operation to track all the changes to user model. my approach is as follows.
class User(AbstractUser):
undergrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
undergrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
postgrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
postgrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
currently_working_on = models.TextField()
previous_work_experience = models.TextField()
previous_internship_experience = models.TextField()
def __str__(self):
return self.username
def save(self, *args, **kwargs):
Log(user=User, actions="Updated profile",
extra={"undergrad_college": self.undergrad_college,
"undergrad_degree": self.undergrad_degree,
"postgrad_college": self.postgrad_college,
"postgrad_degree": self.postgrad_degree,
"currently_working_on": self.currently_working_on,
"previous_work_experience": self.previous_work_experience,
"previous_internship_experience": self.previous_internship_experience
})
super(User, self).save(args, **kwargs)
my views are like this for handling the logging.
class ActivityMixin(LoginRequiredMixin):
def get_context_data(self, **kwargs):
context = super(ActivityMixin, self).get_context_data(**kwargs)
context['activities'] = Log.objects.filter(user=self.request.user)
return context
class IndexListView(ActivityMixin, ListView):
template_name = 'pages/home.html'
model = User
I get this error while performing the update action.
Cannot assign "<class 'users.models.User'>": "Log.user" must be a "User" instance.
Update view is as follows
class UserUpdateView(LoginRequiredMixin, UpdateView):
form_class = UserForm
# we already imported User in the view code above, remember?
model = User
# send the user back to their own page after a successful update
def get_success_url(self):
return reverse("users:detail",
kwargs={"username": self.request.user.username})
def get_object(self, **kwargs):
# Only get the User record for the user making the request
return User.objects.get(username=self.request.user.username)
How to assign the User model instance to the Log function. I cant get this working. I am Django newbie.
Looks like pretty straightforward, replace User with self:
Log(user=User, ...
Log(user=self, ...