Migrating to django-guardian permissions with custom User class - django

I am trying to migrate my models to use Guardian permissions. At this point I have:
class Data(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta:
permissions = (
('view', 'View'),
('edit', 'Edit'),
('owner', 'Owner'),
)
I created one migration that added the new permissions, and in a custom migration I am trying to assign the permissions like this:
def assignDataPermissions(apps, schema_editor):
Data = apps.get_model('api', 'Data')
for data in Data.objects.all():
assign_perm('api.owner', data.user, data)
class Migration(migrations.Migration):
dependencies = [
('api', '0169_auto_20180304_1619'),
]
operations = [
migrations.RunPython(assignDataPermissions)
]
This fails with
guardian.exceptions.NotUserNorGroup: User/AnonymousUser or Group instance is required (got EmailUser object).
Is there a better/proper way of migrating to Guardian? If not, how do I make it see my custom User class?

I ended up using higher level workaround.
Inside the migration, data.user is actually an object of __fake.EmailUser, while get_user_model() returns custom_user.models.EmailUser. As a result Guardian fails the check isinstance(identity, get_user_model()).
My workaround(hack?) is to explicitly get the EmailUser object from the database corresponding to data.user. Like so:
def assignDataPermissions(apps, schema_editor):
Data = apps.get_model('api', 'Data')
User = get_user_model()
for data in Data.objects.all():
user = User.objects.get(id=data.user_id)
assign_perm('api.owner', user, data)

In general, loading external libraries in migrations is prone to errors.
Try something low-level like that:
def assignDataPermissions(apps, schema_editor):
Data = apps.get_model('api', 'Data')
Permission = apps.get_model('auth', 'Permission')
owner_api = Permission.objects.get(content_type__applabel='api', codename='owner')
UserObjectPermission = apps.get_model('guardian', 'UserObjectPermission')
for data in Data.objects.all():
UserObjectPermission.objects.create(permission=owner_api, user=data.user, content_object=data)

Related

How to let user fetch only those objects they are related to (by many-to-many relationship)?

I have a project in which there are workspaces, and each workspace can have any number of users. Users can also belong to multiple workspaces, so there's a many-to-many relationship between them.
Now, I have workspace creation and membership working, but I'm having trouble setting the permissions so that only the workspace members can see the workspace. I've tried with a custom object-level permission, but it doesn't seem to work.
The workspace model looks like this:
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
The view looks like this:
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all().order_by('name')
serializer_class = WorkspaceSerializer
permission_classes = [permissions.IsAuthenticated|BelongsInWorkspace]
The serializer is like this:
class WorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = Workspace
fields = ('name', 'users')
def create(self, validated_data):
instance = super(WorkspaceSerializer, self)
instance.users.add(self.context['request'].user)
return instance
And finally, the custom permission I'm trying to use here:
class BelongsInWorkspace(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return obj.users.filter(pk=request.user).exists()
I would highly recommend django-guardian to handle this problem. Django-guardian allows for straightforward, object-level permission management.
To handle your problem, all you would need to do is add a Meta class to your workspace model, where you can create custom permissions.
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
class Meta:
default_permissions = ('add', 'change', 'delete')
permissions = (
('access_workspace', 'Access workspace'),
)
Assign either a user or a group that permission, attached to a specific workspace.
assign_perm('access_workspace', user, workspace)
#This will return True if the user has been properly assigned the permission
user.has_perm('access_workspace', workspace)
Then to get all workspaces a user has access to, you would just need to call get_objects_for_user() in your view
queryset = get_objects_for_user(self.request.user, 'project.access_workspace')

Edit update all schedules except primary key in Django Rest Framework

I wanna change all fields of a json object except 'pk' in DRF. I just need to keep one json data. When adding a new data ,this one should override existing data. Is there a way to do it with django ?
my models.py
class ClientUser2(models.Model):
phone_number = models.CharField(max_length=20,unique=True)
name = models.CharField(max_length=100,blank=True)
status = models.IntegerField(default=1)
class ClientNameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ClientUser2
fields = ('url','phone_number','name','status','pk')
my views.py
class ClientViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows messages to be viewed or edited.
"""
queryset = ClientUser2.objects.all()
serializer_class = ClientNameSerializer
and it's my api root
api_root
If you want to be able to only retrieve and update models you can use RetrieveUpdateApiView
Reference : https://www.django-rest-framework.org/api-guide/generic-views/#retrieveupdateapiview

Django - Is this possible that the add default values when model is creating?

I want to add some default values in my database when the related model is creating with makemigrations command.
For example I have this as model;
class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created Date')
modified_at = models.DateTimeField(auto_now=True, verbose_name='Update Date')
is_deleted = models.BooleanField(default=False, verbose_name='Deleted')
class Meta:
abstract = True
class ModelType(BaseModel):
description = models.CharField(verbose_name='Name', max_length=225 )
and as I said before I want to add some default values ("value1", "value2", "value3", "value4") for my ModelType table. Is that possible?
If you want to always add the default data when you execute a given migration, the safest way is to use a datamigration (as suggested by #Kos).
To create a data migration, use ./manage.py makemigrations <app_label> --empty and manually add the required code to populate the data.
I normally use a custom operation which executes a get_or_create on the specified model. Add this code to either the migration file itself or somewhere where it can be imported from:
from django.db import migrations
def noop(apps, schema_editor):
pass
class EnsureInstanceCreated(migrations.RunPython):
def __init__(self, app_name, model_name, attrs, defaults=None):
super(EnsureInstanceCreated, self).__init__(self.add_instance, noop)
self.app_name = app_name
self.model_name = model_name
self.attrs = attrs
self.defaults = defaults
def add_instance(self, apps, schema_editor):
Model = apps.get_model(self.app_name, self.model_name)
Model.objects.get_or_create(
defaults=self.defaults,
**self.attrs
)
Then, in the migration itself:
from django.db import migrations
from myproject.utils.migrations import EnsureInstanceCreated
class Migration(migrations.Migration):
dependencies = [
('myproject', '000x_auto_...'),
]
operations = [
EnsureInstanceCreated('myapp', 'ModelType', attrs={
'description': 'value1',
}, defaults={
# ...
}),
EnsureInstanceCreated('myapp', 'ModelType', attrs={'description': 'value2'}),
EnsureInstanceCreated('myapp', 'ModelType', {'description': 'value3'}),
]
Apparently there is a way. You can use Fixtures to initialize models with data.
Refer to this piece of documentation: https://docs.djangoproject.com/en/1.10/howto/initial-data/

django model - fetching user data accross multiple tables

I am writing a django (1.10) website and using allauth for authorisations. I don't want to extend the user model in django - because allauth adds a further layer of complexity to what is already a seemingly convoluted process.
I want to create a model (Custom UserManager?) that will have the following methods:
get_all_subscriptions_for_user(user=specified_user)
get_unexpired_subscriptions_for_user(user=specified_user)
Note: unexpired subscriptions are defined by subscriptions whose end_date > today's date.
This is a snippet of my models.py below
from django.db import models
from django.contrib.auth.models import User
#...
class Subscription(models.Model):
token = models.CharKey()
start_date = models.DateTime()
end_date = models.DateTime()
# other attributes
class UserSubscription(models.Model):
user = models.ForeignKey(User)
subscription = models.ForeignKey(Subscription)
# In view
def foo(request):
user = User.objects.get(username=request.user)
# how can I implement the following methods:
# get_all_subscriptions_for_user(user=specified_user)
# get_unexpired_subscriptions_for_user(user=specified_user)
Ideally, I would like to have a custom user manager, which can fetch this data in one trip to the database - but I'm not sure if I can have a custom user manager without having a custom user model.
[[Aside]]
I'm trying to avoid using a custom model, because it wreaks havoc on the other applications (in my project) which have User as a FK. makemigrations and migrate always barf with a message about inconsistent migration history
You can go with a custom Manager, don't need a UserManager since you are fetching related models:
class UserSubscriptionManager(models.Manager):
def for_user(self, user):
return super(UserSubscriptionManager, self).get_queryset().filter(user=user)
def unexpired_for(self, user):
return self.for_user(user).filter(
suscription__end_date__gt=datetime.date.today() # import datetime
)
in your models:
class UserSubscription(models.Model):
user = models.ForeignKey(User)
subscription = models.ForeignKey(Subscription)
user_objects = UserSubscriptionManager()
this way you can do chain filters in the view, for example:
unexpired_suscriptions = UserSubscription.user_objects().unexpired_for(
user=request.user
).exclude(suscription__token='invalid token')
Try this:
response = []
user_sub = UserSubscription.objects.filter(user=user.pk)
for row in user_sub:
subscription = Subscription.objects.get(pk=row.subscription)
end_date = subscription.end_date
if end_date > timezone.now():
response.append(subscription)

Django migrations RunPython not able to call model methods

I'm creating a data migration using the RunPython method. However when I try to run a method on the object none are defined. Is it possible to call a method defined on a model using RunPython?
Model methods are not available in migrations, including data migrations.
However there is workaround, which should be quite similar to calling model methods. You can define functions inside migrations that mimic those model methods you want to use.
If you had this method:
class Order(models.Model):
'''
order model def goes here
'''
def get_foo_as_bar(self):
new_attr = 'bar: %s' % self.foo
return new_attr
You can write function inside migration script like:
def get_foo_as_bar(obj):
new_attr = 'bar: %s' % obj.foo
return new_attr
def save_foo_as_bar(apps, schema_editor):
old_model = apps.get_model("order", "Order")
for obj in old_model.objects.all():
obj.new_bar_field = get_foo_as_bar(obj)
obj.save()
Then use it in migrations:
class Migration(migrations.Migration):
dependencies = [
('order', '0001_initial'),
]
operations = [
migrations.RunPython(save_foo_as_bar)
]
This way migrations will work. There will be bit of repetition of code, but it doesn't matter because data migrations are supposed to be one time operation in particular state of an application.
did you call your model like said in the documentation ?
def combine_names(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Person = apps.get_model("yourappname", "Person")
for person in Person.objects.all():
person.name = "%s %s" % (person.first_name, person.last_name)
person.save()
Data-Migration
Because at this point, you can't import your Model directly :
from yourappname.models import Person
Update
The internal Django code is in this file django/db/migrations/state.py
django.db.migrations.state.ModelState#construct_fields
def construct_fields(self):
"Deep-clone the fields using deconstruction"
for name, field in self.fields:
_, path, args, kwargs = field.deconstruct()
field_class = import_string(path)
yield name, field_class(*args, **kwargs)
There is only fields that are clones in a "fake" model instance:
MyModel.__module__ = '__fake__'
Github Django
The fine print is laid in Historical Models
Because it’s impossible to serialize arbitrary Python code, these historical models will not have any custom methods that you have defined.
It was quite a surprise when I first encountered it during migration and didn't read the fine print because it seems to contradict their Design Philosophy (adding functions around models)
As of Django 1.8, you can make model managers available to migrations by setting use_in_migrations = True on the model manager. See the migrations documentation.
This does not answer the OP, but might still be of use to someone.
Not only are custom model methods unavailable in migrations, but the same holds for other model attributes, such as class "constants" used for model field choices. See examples in the docs.
In this specific edge case, we cannot access the historical values of the choices directly, during migration, but we can get the historical values from the model field, using the model _meta api, because those values are contained in migrations.
Given Django's Student example:
class Student(models.Model):
FRESHMAN = 'FR'
...
YEAR_IN_SCHOOL_CHOICES = [(FRESHMAN, 'Freshman'), ...]
year_in_school = models.CharField(
max_length=2,
choices=YEAR_IN_SCHOOL_CHOICES,
default=FRESHMAN,
)
We can get the historic value of Student.FRESHMAN inside a migration as follows:
...
Student = apps.get_model('my_app', 'Student')
YEAR_IN_SCHOOL_CHOICES = Student._meta.get_field('year_in_school').choices
...
Something useful that worked for me when you have many complex methods calling each other and you need them available via your object:
First copy those model methods over into your migration file
def A(self):
return self.B() + self.C()
def B(self):
return self.name
def C(self):
return self.description
Then in your migration function:
def do_something_to_your_objects(apps, schema_editor):
MyModel = apps.get_model("my_app", "MyModel")
MyModel.A = A
MyModel.B = B
MyModel.C = C
for my_object in MyModel.objects.all():
my_object.name_and_decription = my_object.C()
my_object.save()
class Migration(migrations.Migration):
dependencies = [
('initial', '0001_initial'),
]
operations = [
migrations.RunPython(do_something_to_your_objects)
]
If you are like me, who came here because you got the error ValueError: RunPython must be supplied with a callable
It's because you put "()" at the end of the function that you are assigning to code in migrations.RunPython
Error e.g. migrations.RunPython(code=do_something(), reverse=noop)
It should be:
migrations.RunPython(code=do_something, reverse=noop) without the ()