Validating an object with a ManytoMany relation requirement - django

I am struggling with a validation problem while using a manytomany relation in Django.
class CourseGroup(models.Model):
name = models.CharField(max_length=255)
color = models.Charfield(max_length=255, choices=(
('blue', 'Blue'), ('red', 'Red'),
('white', 'White')),
helpt_text='Education level')
class Course(models.Model):
name = models.CharField(max_length=255)
location = models.CharField(max_length=255)
floor = models.IntegerField()
courses = models.ManyToManyField('Course', related_name='courses')
class Teacher(models.Model):
course_groups = models.ManyToManyField('Course', related_name='teachers')
name = models.CharField(max_length=255)
level = models.Charfield(max_length=1, choices=(
('1', 'Level 1'), (2, 'Level 2')), helpt_text='Education level')
def validate_courses(self):
if self.level == '1':
groups = self.course_groups.all().prefetch_related('courses')
color_groups = groups.filter(color__in=['red', 'blue'])
not_has_red_or_blue = color_groups.count() < 1
if not_has_red_or_blue:
raise ValidationError(NotHasRedOrBlueError)
for group in groups:
courses = group.courses.filter(floor__eq=2)
floor_incorrect = courses.count() < 1
if floor_incorrect:
raise ValidationError(FloorNotMatchingError)
def save(self, course_list, **kwargs):
with transaction.atomic:
super(Teacher, self).save()
self.course.add(course_list)
self.validate_courses()
My problem is this:
Teachers can only be saved when they are validated in relation to their CourseGroups:
A Teacher having level 1, should have a CourseGroup with color blue or red related. When the teacher has level 2, it doesn't matter, anything goes.
Next to that, when having level 1, the CourseGroup should always contain a Course that has a floor value of 2.
When saving the Teacher, it does not know which Courses are about to be related. When creating, it doesn't even have an Course.id, when it does, it doesn't know which relations will come after the save().
Possible solution for now: adjust the save() method, giving a list of Courses. Using transaction.atomic save the object, set the relations and validate the object with that state. If it doesn't validate, raise validation error and rollback.
Problem now remaining is the validation on the second requirement. On saving the Teacher it might validate ok on the CourseGroup because every group contains a Floor2 Course. But what to do when after saving the teacher, somewhere a Course gets removed from the CourseGroup, and the Teacher doesn't validate anymore?
Is there anyone with a better solution for this problem?

Related

Filter objects from a table based on ManyToMany field

I am working on a messaging app.
The models are as follows.
class ChatRoom(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
room_name = models.CharField(max_length=100, null=True, blank=True)
participants = models.ManyToManyField(User)
class Meta:
db_table = TABLE_PREFIX + "chat_room"
class Message(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
chat_room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE)
message = models.TextField()
sent_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_on = models.DateTimeField(default=django.utils.timezone.now)
class Meta:
db_table = TABLE_PREFIX + "chat_message"
I have to filter ChatRoom object which contains a list of users.
Eg: On sending data like this to the api
{
"participants": [2,3]
}
It should return a ChatRoom object which contains both user '2' and user '3' as participants.
I had tried filtering it with
room = ChatRoom.objects.filter(participants__in=serializer.validated_data['participants'])
but it returns a list of room objects with user '1' and user '2'.
I had also tried using get() but it returns an error.
What am I doing wrong and how can i do it right?
Thanks for looking into my problem. Hope you have a solution.
You said you are using Query..
ChatRoom.objects.filter(participants__in=serializer.validated_data['participants'])
filter function returns a list of objects of the model matching the conditions.
get function only return a single object of model and if none or more than one item matches the condition then an exception is thrown. That is why get is only used for candidate keys or unique resultant conditions.
In your case, I suppose both participants can be part of many rooms, also there might be a possibility that there is no such room. if you want only one queryset to be returned you can use this.
room_list= ChatRoom.objects.filter(participants__in=serializer.validated_data['participants']
if len(room_list) >0:
room = room_list[0]
else:
room = None
This will return the first room in which all the given participants are present, you can replace 0 with any valid value or add something more in the query if you want it to return a specific room.

How to make two manytomany fields in a model hold different values

I have Two simplified models as bellow:
class Courses:
name = models.CharField(_("Course name"), max_length=256)
class School:
main_courses = models.ManyToManyField(_("Main Courses"), to="course.Courses", related_name="maincourses", blank=True)
enhancement_courses = models.ManyToManyField(_("Enhancement Courses"), to="course.Courses" related_name="enhancementcourses", blank=True)
def clean(self) -> None:
#check if selected items in main_courses and enhancement_courses are not equal
status = [True for course in self.main_courses.all() if course in self.enhancement_courses.all()]
if any(status):
raise ValidationError("Chosen contents should not be equal")
return None
main_courses and enhancement_courses are going to hold a list of Courses. But I need to make sure their values wont be equal. For example if in school_1, main_courses are math, physics then enhancement_courses can't be these values. What is the simplest way in django to do this?
Update
I need to validate when user select items in the fields main_courses and enhancement_courses in School table, after saving the changes, model should verify the chosen items in those two fields are not equal. At the moment, there is a bug in clean() method that it keeps the values of first validation/save. For example it raises error if main_courses and enhancement_courseschosen items are equal, but after deselecting, again it raises error.
You can utilize ManyToMany.limit_choices_to to constrain only certain items for each field:
class CourseCategory(models.TextChoices):
MAIN = "main", "Main"
ENHANCHEMENT = "enhancement", "Enhancement"
class Course(models.Model):
name = models.CharField(_("Course name"), max_length=256)
category = models.CharField(_("Course Category"), max_length=32, choices=CourseCategory.choices)
class School(models.Model):
main_courses = models.ManyToManyField(_("Main Courses"), to="course.Courses", related_name="maincourses", blank=True, limit_choices_to=Q(category=CourseCategory.MAIN))
enhancement_courses = models.ManyToManyField(_("Enhancement Courses"), to="course.Courses" related_name="enhancementcourses", blank=True, limit_choices_to=Q(category=CourseCategory.ENHANCEMENT))

Annotating values from filtered related objects -- Case, Subquery, or another method?

I have some models in Django:
# models.py, simplified here
class Category(models.Model):
"""The category an inventory item belongs to. Examples: car, truck, airplane"""
name = models.CharField(max_length=255)
class UserInterestCategory(models.Model):
"""
How interested is a user in a given category. `interest` can be set by any method, maybe a neural network or something like that
"""
user = models.ForeignKey(User, on_delete=models.CASCADE) # user is the stock Django user
category = models.ForeignKey(Category, on_delete=models.CASCADE)
interest = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)])
class Item(models.Model):
"""This is a product that we have in stock, which we are trying to get a User to buy"""
model_number = models.CharField(max_length=40, default="New inventory item")
product_category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Category")
I have a list view showing items, and I'm trying to sort by user_interest_category for the currently logged in user.
I have tried a couple different querysets and I'm not thrilled with them:
primary_queryset = Item.objects.all()
# this one works, and it's fast, but only finds items the users ALREADY has an interest in --
primary_queryset = primary_queryset.filter(product_category__userinterestcategory__user=self.request.user).annotate(
recommended = F('product_category__userinterestcategory__interest')
)
# this one works great but the baby jesus weeps at its slowness
# probably because we are iterating through every user, item, and userinterestcategory in the db
primary_queryset = primary_queryset.annotate(
recommended = Case(
When(product_category__userinterestcategory__user=self.request.user, then=F('product_category__userinterestcategory__interest')),
default=Value(0),
output_field=IntegerField(),
)
)
# this one works, but it's still a bit slow -- 2-3 seconds per query:
interest = Subquery(UserInterestCategory.objects.filter(category=OuterRef('product_category'), user=self.request.user).values('interest'))
primary_queryset = primary_queryset.annotate(interest)
The third method is workable, but it doesn't seem like the most efficient way to do things. Isn't there a better method than this?

Manytomany field is None and can't create or add field to model. How do I add the field?

My models are set up as follows:
class Person(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='person_user')
type = models.IntegerField()
class Score(models.Model):
person = models.ForeignKey(Person, related_name='person')
score = models.FloatField(default=0)
I can create Score objects fine and create a relation to the Person. However, the next part is causing some difficulty. I added Score after I had already created the following fields (except for the very last line):
class Applicant_Response(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='user')
interview = models.ForeignKey(Interview, related_name='interviews')
extra_information = models.ForeignKey(ExtraInformation, related_name='extra_information', null=True, blank=True)
score = models.ManyToManyField(Score, related_name='applicant_score', default=1)
I created a Score object that had a score of 0 and a person to use as the default for score (was assigned an ID of 1). However, when I tried accessing the field score in the Applicant_Response, I get profiles.Score.None, which confuses me (profiles is the name of the application).
To my understanding, I am not able to add anything to the manytomany field because it does not exist? Maybe the way I am trying to add Score to Applicant_Response is incorrect?:
try:
applicants = models.Applicant_Response.objects.filter(interview=interviews)
for applicant in applicants:
applicant.score.add(models.Score.objects.get(id=1))
applicant.save()
print applicant.score
except Exception as e: print e
I get the following in stdout: profiles.Score.None
How do I add a Score to the Applicant_Response object?
I find out this:
# You have to Save() first.
applicant.save()
applicant.score.add(models.Score.objects.get(id=1))
# I think it work???
# If not:
sc1 = models.Score.objects.get(id=1)
applicant.save()
applicant.score.add(sc1)
I hope it help you.

Django - Dynamically creating field names in a model (fields not saving)

I am trying to implement a voting system that keeps track of votes for each type of user on my site. My plan was to create a Vote model that keeps track of Up votes and Total votes for each user type and calculates the percentage of Up votes.
Hardcoded that looks something like this:
class Eduuser(AbstractUser):
TYPE_1 = 'TY1'
TYPE_2 = 'TY2'
...
USER_TYPE_CHOICES = (
(TYPE_1, 'Type 1'),
(TYPE_2, 'Type 2'),
...
)
user_type = models.CharField(max_length=3, choices=USER_TYPE_CHOICES)
class Vote(models.Model):
a = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_1')+'_up')
b = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_2')+'_up')
...
c = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_1')+'_votes')
d = models.IntegerField(
default=0, name=getattr(Eduuser, 'TYPE_2')+'_votes')
...
def perc(self):
perc_array = []
for user_type in getattr(Eduuser, 'USER_TYPE_CHOICES'):
up = float(getattr(self, user_type[0]+'_up')) #Prevent int division
votes = getattr(self, user_type[0]+'_votes')
if votes==0:
perc_array.append(0)
else:
perc_array.append(round(up/votes, 3))
return perc_array
Although I don't anticipate adding more types, I would like for the code to look cleaner. My best attempt at looping over the user types was:
class Eduuser(AbstractUser):
...
class Vote(models.Model):
for user_type in getattr(Eduuser, 'USER_TYPE_CHOICES'):
models.IntegerField(
default=0, name=user_type[0]+'_up')
models.IntegerField(
default=0, name=user_type[0]+'_votes')
def perc(self):
...
However this does not save the fields (I guess because of the lack of assignment operator).
So a couple of quick questions:
1) Is there a way to save fields without explicitly assigning them a name? Or can I convert the string name into a variable (from other posts I've read, this seems like a bad idea)?
2) Am I even approaching this voting idea logically? Part of me feels like there is a far easier approach to keeping track of votes for multiple types of users.
Any help is appreciated! Thanks!
django-model-utils can make this cleaner with it's Choices helper.
You could do a Vote model in the following way (untested):
from model_utils import Choices
class User(AbstractUser):
USER_CHOICES = Choices(
('one', 'Type 1'),
('two', 'Type 2'),
)
user_type = models.CharField(max_length=10, choices=USER_CHOICES)
class Vote(models.Model):
"""
A single vote on a `User`. Can be up or down.
"""
VOTE_CHOICES = Choices(
('upvote'),
('downvote'),
)
user = models.ForeignKey(User)
vote = models.CharField(max_length=10, choices=VOTE_CHOICES)
Example usage – get the number of positive votes for all “Type 1” Users:
# retrieve all of the votes
all_votes = Vote.objects.all()
all_votes_count = len(all_votes)
# now retrieve all of the votes for users of ‘Type 1’
type_one_votes = all_votes.filter(user__user_type=User.USER_CHOICES.one)
type_one_votes_count = len(type_one_votes)
# …and now filter the positive votes for ‘Type 1’ users
type_one_positives = type_one_votes.filter(vote=Vote.VOTE_CHOICES.upvote)
type_one_positive_vote_count = len(type_one_positives)
# etc.
Django uses some metaclass behavior to create fields based on what you declare, so this is not wholly trivial. There are some undocumented calls you can use to dynamically add fields to your model class - see this post:
http://blog.jupo.org/2011/11/10/django-model-field-injection/
That said, I would recommend a simpler approach. Create a model to hold your possible user types, then use it as a foreign key in the votes table:
class UserType(models.Model):
type_name = models.CharField()
class Vote(models.Model):
user_type = models.ForeignKey(UserType)
total = models.PositiveIntegerField()
Or track the individual votes and sum as needed, either recording the user who cast the vote or just the user's type at the time the vote was cast. Depending on what you want to do if a user changes classes after voting you might need to save the user's type anyway.
If you do just track the sums, you have to think more carefully about transaction issues - I'd say track the user and enforce a uniqueness constraint.