how to avoid circular references and write DRY code in django - python

Whenever I create DRY functions that I can reuse later and then use them in models, I get circular references;
For example:
I have the following models:
from social.services import get_top_viewed_posts
class Post(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
title = models.CharField('Post Title', max_length=255)
class ActivityUpdateEmail(models.Model):
sent = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now = True)
def send(self):
posts = get_top_viewed_posts()
My top viewed posts function is another file called services.py so I can access it other places. It looks like:
from social.models import Post
def get_top_viewed_posts():
posts = Post.objects.filter(
pk__in=popular_posts_ids,
).order_by(
'-created_at'
)
return posts
Then I get the error:
services.py", line 1, in <module>
from social.models import Post
ImportError: cannot import name 'Post'
If I change it to:
transactions = Action.objects.filter(
content_type__pk=35,
created_at__gte=start_date,
).values_list('object_id', flat=True)
popular_posts_ids = []
popular_posts = Counter(transactions).most_common()[:result_amount]
for dic in popular_posts:
popular_posts_ids.append(dic[0])
class ActivityUpdateEmail(models.Model):
sent = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now = True)
def send(self):
posts = Post.objects.filter(
pk__in=popular_posts_ids,
).order_by(
'-created_at'
)
This works no problem.
How can I use this dry approach of abstracting functionality, and then being able to use them in my models?

The error occurs because when you import get_top_viewed_posts at the top of models.py the Post model is not declared yet.
You have a few alternatives.
Move the import from the top of models.py to inside the method
def send(self):
from social.services import get_top_viewed_posts
posts = get_top_viewed_posts()
Don't worry about performance, imports are cached - but if you use it in other methods it may be tedious to repeat the same import over and over.
Abstract the class
Make the function more generic passing the model as an argument, this way you don't need to import the model in the top of the services.py file:
def get_top_viewed_model(model, popular_ids, order_by='-created_at'):
return model.objects..filter(
pk__in=popular_ids,
).order_by(
order
)
Then:
def send(self):
posts = get_top_viewed_model(type(self), popular_posts_ids)
# at other places
get_top_viewed_model(Posts, popular_posts_ids)
Use a custom manager
Create a custom manager with a top_viewed method:
class TopViewedManager(models.Manager):
def __init__(self, order='-created_at', **kwargs):
self._order = order
self._filter = kwargs
def top_viewed(self):
return self.get_queryset().filter(**self._filter).order_by(self._order)
class Post(models.Model):
...
objects = TopViewedManager(pk__in=popular_posts_ids)
Then just use this where you would use get_top_viewed_model:
Post.objects.top_viewed()
This manager is quite generic so you can use it with any model, filter and order you want.
Probably there are other alternatives and it is a matter of personal taste.

Related

why model function created on django 3.1.10 doesn't Work on 3.2?

i created a proxy model in django 3.1.10 where i defined a function to make a copy of an object with all it's related objects through foreign key and it worked fine , but when i upgraded to django 3.2 the function only create a copy of the object without any related objects
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from ValPlanner.models import ProgramExecution
import datetime
from django.utils import timezone
#-----------------------------------------------------------------------------------------------------------------------------
class Record(models.Model):
title = models.CharField(max_length=50)
description = models.CharField(max_length=200)
execution = models.ForeignKey(ProgramExecution, on_delete=models.PROTECT,null=True)
timestamp = models.DateTimeField(auto_now_add=True)
creator = models.ForeignKey(User, on_delete=models.PROTECT)
record_type = models.CharField(max_length=50,default='template')
def __str__(self):
return self.title
class TemplateRecord(Record):
class Meta:
proxy = True
def create_report(self,creator,execution):
stages = self.stage_set.all();
self.record_type = 'record'
self.pk = None;
self.execution = execution
self.creator = creator
self.save();
for stage in stages :
samples = stage.sample_set.all()
stage.pk = None
stage.stage_type = 'record'
stage.record = self
stage.save();
for sample in samples :
tests = sample.testnum_set.all()
sample.sample_type = 'record'
sample.stage = stage
sample.sample_time = timezone.now()
sample.sampler = creator
for samplenum in range(sample.number_of_samples):
sample.pk = None
sample.save();
for test in tests :
test.pk = None
test.test_type = 'record'
test.sample = sample
test.tester = creator
test.save();
#-----------------------------------------------------------------------------------------------------------------------------
class Stage(models.Model):
record = models.ForeignKey(Record, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
stage_type = models.CharField(max_length=50,default='template')
def __str__(self):
return self.title
#-----------------------------------------------------------------------------------------------------------------------------
class Sample(models.Model):
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
sample_time = models.DateTimeField(default=timezone.now)
sample_location = models.CharField(max_length=50)
sampler = models.ForeignKey(User, on_delete=models.PROTECT)
sample_type = models.CharField(max_length=50,default='template')
number_of_samples = models.PositiveSmallIntegerField(null=True,default=1)
def __str__(self):
return self.name
#-----------------------------------------------------------------------------------------------------------------------------
class TestNum(models.Model):
sample = models.ForeignKey(Sample, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
result = models.DecimalField(max_digits=15, decimal_places=5,null=True)
acceptance = models.CharField(max_length=50)
maximum_acceptance = models.DecimalField(max_digits=15, decimal_places=5)
minimum_acceptance = models.DecimalField(max_digits=15, decimal_places=5)
test_type = models.CharField(max_length=50,default='template')
tester = models.ForeignKey(User, on_delete=models.PROTECT)
def __str__(self):
return self.name
#-----------------------------------------------------------------------------------------------------------------------------
and my function is in view.py :
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.models import User
from django.urls import reverse
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin, PermissionRequiredMixin
from .models import Record, TemplateRecord, Stage, Sample, TestNum
import datetime
from django.utils import timezone
class CreateEmptyReport (PermissionView, UpdateView):
model = TemplateRecord
permission_required = 'ETRecord.add_record'
fields = ['execution']
def get_object(self):
template_list = TemplateRecord.objects.filter(record_type='template')
return super().get_object(queryset = template_list)
def form_valid(self,form):
self.object.create_report(creator=self.request.user,execution=form.instance.execution)
return super().form_valid(form);
def get_success_url(self):
return reverse('ETRecord:report_details',kwargs= {'pk': self.object.id})
on old version it ould copy the record and all relaated stages , samples and tests but after update it only copies the record with no associated stages or samples
i tried to uninstall and re-install the older verison it works only on the older version , so how can i make it compatable with the new 3.2 version ?
You are creating the queryset before you copy the record itself, which seems fine
stages = self.stage_set.all();
But... Django won't resolve this queryset before it'll be accessed. Older versions of Django would probably just hold the ID of the parent element, so the queryset doesn't change after you change the ID on the self, but newer versions of Django are holding the entire object and extracting the ID of the object just before the queryset is actually executed on the database.
This causes your for loop to iterate over stages connected to the new record instance (you've just created it, so it's an empty queryset) instead of the old one.
To fix that issue, you can either force the execution of the queryset earlier (for example by casting it into a list) or by creating the queryset that won't use the self object, but the ID of the old object explicitly, for example:
stages = Stage.objects.filter(record_id=self.id);

How can I pass my function inside my class model as a field?

I'm using the 3rd party library, Django Extensions, specifically for AutoSlugField().
I want to implement their use of RandomCharField() in my model for my slug field.
from django_extensions.db.fields import AutoSlugField, RandomCharField
class Post(models.Model):
class BucketObjects(models.Manager):
def get_queryset(self):
return super().get_queryset()
...
#unique_string = RandomCharField(length=10, unique=True)
slug = AutoSlugField(populate_from = models.random_string(), blank=True)
...
objects = models.Manager()
bucketobjects = BucketObjects()
def random_string(self):
unique_characters = RandomCharField(length=10, unique=True)
self.slug = unique_characters
self.save()
def __unicode__(self):
return self.stock_list
AutoSlugField requires the populate_from parameter. Passing RandomCharField() like so:
slug = AutoSlugField(populate_from = RandomCharField(), ...)
Does not work:
TypeError: 'populate_from' must be str or list[str] or tuple[str], found <django_extensions.db.fields.RandomCharField>
So instead I want to make a function within my class that creates and saves a RandomCharField to my slug field.
In the model above, you can see I commented out unique_string, if I pass that field to my populate_from parameter, then everything works fine. But it seems a redundant and a waste of space to have two model fields that are exactly the same.
How can I pass my random_string function to my slug populate_from parameter whenever a Post is created?
Thank you for the help.
You don't need to (or can't) use the RandomCharField(...) here. But, something like this will certainly work
from django_extensions.db.fields import AutoSlugField,RandomCharField
import string, random
class Post(models.Model):
slug = AutoSlugField(populate_from="random_string", blank=True)
def random_string(self):
length = 10
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))

Django 3 Dynamic Default Value in Model

There are similar questions but none recent and based on Django 3 so here it goes:
I am using py nanoid to generate some unique IDs so in my models I have
from nanoid import generate
and then
class Work(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published = models.BooleanField(False)
date = models.DateTimeField()
nanoid = models.CharField(max_length=15, default = generate(size=10) )
def __str__ (self):
return self.title
my aim is to ensure that a new nanoid is generated each time a new work post is added. At the moment is the same repeated each time I try to add a new Work via admin.
I read some replies about creating a custom class but I am a bit at a loss here!
If you pass a function as a default it must be one that takes no arguments. You can use functools.partial to turn generate into a function that takes no args and has size=10 set as the default
from functools import partial
class Work(models.Model):
...
nanoid = models.CharField(max_length=15, default=partial(generate, size=10))
You can override the save method on your model to do it, also you have to create a helper function to check if the nanoid on your model already exists in your persistent storage.
class Work(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published = models.BooleanField(False)
date = models.DateTimeField()
nanoid = models.CharField(max_length=15)
def save(self, *args, **kwargs):
nanoid = generate_id()
super(Work, self).save(*args, **kwargs)
def __str__ (self):
return self.title
# Helpers
def generate_id():
"""
Generate nanoid unique code.
"""
n_id = generate(size=10)
while not is_available_code(n_id):
n_id = generate(size=10)
return n_id
def is_available_id(id):
"""
Validate Id.
"""
return not id_is_nanoid(id)
def id_is_nanoid(id):
return Work.objects.filter(nanoid=id).exists()

Populate Django ManyToManyField Options Based on Other Field in Admin

I am having problems filtering options for a ManyToManyField on the Django Admin Add screen based on input to another field on the same form. I am new to Django and have been unable to use any of the generic fixes described elsewhere because they are all slightly different than my situation. Here is my situation:
I have three models in my project: Class, Student, and AttendanceRecord. In the Django Admin, when adding an attendance record, I would like to change the options for the field Absent_Students based on the selection made for the field Associated_Class. So, for example, if Associated_Class "CS 450" is selected, the options for Absent_Students should change to only students whose class_list includes CS 450.
Here are my models:
from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import User
from django.utils.encoding import python_2_unicode_compatible
import random, string
# Create your models here.
#This is the model for a student
#python_2_unicode_compatible
class Student(models.Model):
pass
Student_First_Name = models.CharField(max_length=200)
Student_Last_Name = models.CharField(max_length=200)
Student_ID_Number = models.CharField(max_length=200)
Student_Class = models.ForeignKey('Class', null=True)
def __str__(self):
return self.Student_Last_Name + ',' + self.Student_First_Name
# This is the model for a class
#python_2_unicode_compatible
class Class(models.Model):
class Meta:
verbose_name_plural = "Classes"
Class_Name = models.CharField(max_length=200)
Student_List = models.ManyToManyField('Student', related_name='class_list')
Professor = models.ForeignKey(User,null=True)
AddCode = models.IntegerField
pass
def __str__(self):
return self.Class_Name
def getName(self):
return self.Class_Name
def getProfessor(self):
return self.Professor.id
def getProf(self):
return self.Professor
def getStudents(self):
return self.Student_List
#This is the model for attendance records
class AttendanceRecord(models.Model):
class Meta:
verbose_name = "Attendance Record"
Associated_Class = models.ForeignKey(Class, on_delete=models.CASCADE, related_name='Attendance_Records')
Date = models.DateField()
Absent_Students = models.ManyToManyField('Student', blank=True)
Present_Students = models.ManyToManyField('Student', related_name='a')
def get_associated_class_id(self):
return self.Associated_Class
def __str__(self):
return self.Associated_Class.__str__() + ' on date ' + self.Date.__str__(self)
I have tried doing this by editing the AttendanceRecordAdminForm class and AttendanceRecordAdmin class. My problem is that when setting the self.fields['Absent_Students].queryset I do not know how to access the currently selected Associated_Class on the form. I keep getting an error that "AttendanceRecord has no Associated_Class". Here are those classes just discussed in their entirety:
class AttendanceRecordAdminForm(forms.ModelForm):
class Meta:
model = AttendanceRecord
fields = '__all__'
def __init__(self, *args, **kwargs):
super(AttendanceRecordAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
self.fields['Absent_Students'].queryset = Student.objects.filter(class_list__id=self.instance.get_associated_class_id())
self.fields['Present_Students'].queryset = Student.objects.filter(class_list__id=1)
class AttendanceRecordAdmin(admin.ModelAdmin):
form = AttendanceRecordAdminForm
filter_horizontal = ('Absent_Students', 'Present_Students',)
Basically, I am looking for a way to access the currently entered Associated_Class on the admin form so I can properly filter the queryset.
After hours more of online searching I finally found what I needed. A chained ManyToMany from the smart_select app makes this very easy. This link: How to use django-smart-select describes the install process and also links to the documentation for using it once it is installed. Hopefully this helps some others as well.

Basic Python, Django, DRY - calling a method from a (model) class

I'm new to Python and Django. I have a basic python/django ORM question that's bothering me. I have two models and they have a show_image function that's repeated. That's no good.
class Dinner(models.Model):
title = models.CharField(max_length=200)
is_approved = models.BooleanField()
hero = models.ImageField(upload_to="heros", blank=True)
def show_image(self):
image_url = None
if self.hero is not None:
image_url = """<img src="{0}{1}" />""".format(BASE_URL, self.hero)
return image_url
show_image.short_description = "Thumbnail"
show_image.allow_tags = True
class Speaker(models.Model):
title = models.CharField(max_length=200)
biography = models.TextField(blank=True)
headshot = models.ImageField(upload_to="headshots", blank=True)
def show_image(self):
image_url = None
if self.headshot is not None:
image_url = """<img src="{0}{1}" />""".format(BASE_URL, self.headshot)
return image_url
show_image.short_description = "Thumbnail"
show_image.allow_tags = True
Seems simple enough- I decided to start experimenting. I created a method in models.py...
def test(obj):
print obj
then in my models I tried:
test(self.hero)
and got this (instead of the value):
django.db.models.fields.files.ImageField
How do I get the value out of this so I can check if the ImageField has been populated?
edit:
class Speaker(models.Model):
title = models.CharField(max_length=200)
biography = models.TextField(blank=True)
headshot = models.ImageField(upload_to=upload_to, blank=True)
test(headshot)
def show_image(self):
image_url = None
if self.headshot is not None:
image_url = """<img src="{0}{1}" />""".format(BASE_URL, self.headshot)
return image_url
show_image.short_description = "Thumbnail"
show_image.allow_tags = True
You're calling that test method at class level, which makes no sense. That means it's executed at the time that the model class is defined, which is why you see the field class. There's a whole lot of metaclass stuff that happens when models are defined, so that when you get an instance you see the value, not the field class - but that hasn't happened at the point you're calling the method.
In any case, you need to call that with an instance of the model, so that there is actually a value to deal with.
I suspect you're fairly new to Python, so here's a tip: you can inspect all this stuff from the Python shell. Start ./manage.py shell, then import your models, instantiate one (or get it from the db), then you can examine it with dir() and so on. Much more efficient than writing debug functions in your code.

Categories

Resources