Related
I'm trying to achive an annotated query and here's my code.
# models.py
STATUS_CHOICES = (
("submitted", "제출됨"),
("in-review", "검토중"),
("rejected", "반려됨"),
("approved", "승인됨"),
)
class ClassReview(models.Model):
class = models.ForeignKey("class.Class", on_delete=models.PROTECT)
status = models.CharField(max_length=10, choices=STATUS_CODE, default="submitted")
class ClassReviewVote(models.Model):
target = models.ForeignKey("class.ClassReview", on_delete=models.PROTECT)
vote = models.BooleanField(null=True)
# selectors.py
def get_classreview_set(class_id):
review_set = ClassReview.objects.filter(class_id=class_id, status="approved")
review_Set = review_set.annotate(
vote_up_count=Subquery(
ClassReviewVote.objects.filter(target_id=OuterRef("pk"), vote=True)
.values("target")
.annotate(count=Count("target"))
.values("count"),
output_field=IntegerField(),
)
)
return review_set
# serializers.py
def classreview_serializer(classreview):
data = dict()
...
data["vote_up_count"] = classreview.vote_up_count
...
return data
# views.py
class ClassDetailView(TemplateView):
...
def get(self, request, *args, **kwargs):
class_id = request.kwargs.get("class_id")
review_set = get_classreview_set(class_id)
context = {
review_list = classreview_serializer(review_set)
}
...
...
I want to annotate vote up count of each review.
But it keeps raising Error "more than one row returned by a subquery used as an expression".
When logged in user and someone else voted up. What is happening?
I found why error occurred. There is ordering in model's META class. So it didn't work well.
I guess error occurred when GROUP_BY happens, cuz there is ordering already.
Here's code:
# selectors.py
def get_classreview_set(class_id):
review_set = ClassReview.objects.filter(class_id=class_id, status="approved")
review_Set = review_set.annotate(
vote_up_count=Subquery(
ClassReviewVote.objects.filter(target_id=OuterRef("pk"), vote=True)
.order_by("target") # <------ NEW LINE
.values("target")
.annotate(count=Count("target"))
.values("count"),
output_field=IntegerField(),
)
)
return review_set
It works!!
I don't know what's the problem exactly until now. But I fixed it.
If there is someone knows what's happening exactly, let me know plz.
I have two models Pick and GamePick. GamePick has a ForeignKey relation to Pick, which is accessible on Pick.game_picks.
I have setup GamePick with a custom queryset and manger so that when ever I retrieve a GamePick with the manager objects is is annotated with a field is_correct based on the values of other fields.
Now what I want to be able to do is count the how many correct GamePicks are pointing to a specific Pick.
One simple way is doing this with a method in Python:
class Pick(models.Model):
...
def count_correct(self):
return self.game_picks.filter(is_correct=True).count()
So far so good.
But now, I would like to annotate each Pick with that count, say as correct_count. This is so I can order the Pick with something like Pick.objects.all().order_by("correct_count").
Now how would I do this?
This is where I am:
correct_game_picks = GamePick.objects.filter(
pick=models.OuterRef("pk"),
is_correct=True
)
picks = Pick.objects.annotate(
correct_count=models.Count(correct_game_picks.values("pk"))
)
This is what pick.query gives me:
SELECT
"picks_pick"."id",
"picks_pick"."picker",
"picks_pick"."pot_id",
COUNT((
SELECT U0."id" FROM "picks_gamepick" U0
INNER JOIN "games_game" U1 ON (U0."game_id" = U1."id")
WHERE ((U0."picked_team_id" = U1."winning_team_id") AND U0."pick_id" = "picks_pick"."id")
)) AS "correct_count"
FROM "picks_pick"
GROUP BY "picks_pick"."id", "picks_pick"."picker", "picks_pick"."pot_id"
I am not good at SQL, but it seems like it should be correct.
In my test, it returns 1 when it should 2 for two correct GamePick belonging to a Pick.
Does anybody have any pointers?
Btw, if I remove the .values("pk") I get this error:
E django.db.utils.OperationalError: sub-select returns 5 columns - expected 1
I am not sure why it matters how many column I have when I want to count rows.
As feedback suggests that this is hard to debug without knowing the models, here they are:
class Pot(models.Model):
name = models.CharField(max_length=250, null=False, blank=False)
class Team(models.Model):
name = models.CharField(max_length=250, null=False, blank=False)
class Game(models.Model):
teams = models.ManyToManyField(
Team,
related_name="+",
)
winning_team = models.ForeignKey(
Team,
on_delete=models.CASCADE,
related_name="+",
blank=True,
null=True,
)
class Pick(models.Model):
picker = models.CharField(max_length=100, help_text="Name of the person picking")
# This is the method is would like to replace with an annotation
def count_correct_method(self):
return self.game_picks.filter(is_correct=True).count()
class GamePickQueryset(models.QuerySet):
def annotate_is_correct(self):
return self.annotate(
is_correct=models.ExpressionWrapper(
models.Q(picked_team=models.F("game__winning_team")),
output_field=models.BooleanField(),
)
)
class GamePickManager(models.Manager):
def get_queryset(self):
queryset = GamePickQueryset(self.model, using=self._db)
queryset = queryset.annotate_is_correct()
return queryset
GamePickMangerFromQueryset = GamePickManager.from_queryset(GamePickQueryset)
class GamePick(models.Model):
pick = models.ForeignKey(
Pick, on_delete=models.CASCADE, related_name="game_picks", null=True, blank=True
)
game = models.ForeignKey(Game, on_delete=models.CASCADE,
related_name="game_picks")
picked_team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name="+", null=True, blank=False
)
objects = GamePickMangerFromQueryset()
With these models, I am running this as a test in which I am trying to get the annotation working
team_1 = Team(name="Test Team 1")
team_1.save()
team_2 = Team(name="Test Team 2")
team_2.save()
team_3 = Team(name="Test Team 3")
team_3.save()
team_4 = Team(name="Test Team 4")
team_4.save()
team_5 = Team(name="Test Team 5")
team_5.save()
team_6 = Team(name="Test Team 6")
team_6.save()
assert Team.objects.count() == 6
pot = Pot(name="Test Pot")
pot.save()
assert Pot.objects.count() == 1
assert Pot.objects.first() == pot
game_1 = Game(pot=pot)
game_1.save()
game_1.teams.add(team_1, team_2)
game_1.winning_team = team_1
game_1.save()
game_2 = Game(pot=pot)
game_2.save()
game_2.teams.add(team_3, team_4)
game_2.winning_team = team_3
game_2.save()
game_3 = Game(pot=pot)
game_3.save()
game_3.teams.add(team_5, team_6)
game_3.winning_team = team_5
game_3.save()
assert Game.objects.count() == 3
assert pot.games.count() == 3
assert pot.games.all()[0].winning_team == team_1
assert pot.games.all()[1].winning_team == team_3
assert pot.games.all()[2].winning_team == team_5
pick = Pick(picker="Tester", pot=pot)
pick.save()
assert Pick.objects.count() == 1
game_pick_1 = GamePick(pick=pick, game=game_1, picked_team=team_1)
game_pick_1.save()
game_pick_2 = GamePick(pick=pick, game=game_2, picked_team=team_3)
game_pick_2.save()
game_pick_3 = GamePick(pick=pick, game=game_3, picked_team=team_6)
game_pick_3.save()
assert GamePick.objects.count() == 3
assert pick.game_picks.count() == 3
assert pick.game_picks.all()[0].is_correct == True
assert pick.game_picks.all()[1].is_correct == True
assert pick.game_picks.all()[2].is_correct == False
assert pick.count_correct() == 2
from django.db import models
correct_game_picks = GamePick.objects.filter(
pick=models.OuterRef("pk"),
is_correct=True,
)
pick = Pick.objects.all().annotate(
correct_count=models.Count(
# models.Q(game_picks__in=correct_game_picks)
models.Q(game_picks__picked_team=models.F("game_picks__game__winning_team"))
)
)[0]
assert pick.correct_count == 2
In this test I get 3 == 2. For some reason, it is counting all the game_picks not only the ones that fulfill the expression.
Really don't know what to do with that anymore...
I just realized (thanks to #BradMeinsberger), since I am doing that __in expression, I should not really need the OuterRef.
So the annotation can be just this:
correct_game_picks = GamePick.objects.filter(
is_correct=True,
)
pick = Pick.objects.all().annotate(
correct_count=models.Count(
models.Q(game_picks__in=correct_game_picks)
)
)[0]
But now the kicker: without the OuterRef I can evaluate the correct game picks separately:
assert correct_game_picks.count() == 2
assert pick.correct_count == 2
The first assert passes but the second does not with 3 == 2 😧
How can there be more than 2 in a list of 2?
Is there some kind of duplicate happening?
Now I can through a distinct=True into the Count and it passes 🎉
Let's test another combination e.g. only 1 correct game pick:
game_pick_1 = GamePick(pick=pick, game=game_1, picked_team=team_1)
game_pick_1.save()
game_pick_2 = GamePick(pick=pick, game=game_2, picked_team=team_4)
game_pick_2.save()
game_pick_3 = GamePick(pick=pick, game=game_3, picked_team=team_6)
game_pick_3.save()
assert GamePick.objects.count() == 3
assert pick.game_picks.count() == 3
assert pick.game_picks.all()[0].is_correct == True
assert pick.game_picks.all()[1].is_correct == False
assert pick.game_picks.all()[2].is_correct == False
assert pick.count_correct() == 1
from django.db import models
correct_game_picks = GamePick.objects.filter(
is_correct=True,
)
pick = Pick.objects.all().annotate(
correct_count=models.Count(
models.Q(game_picks__in=correct_game_picks),
distinct=True
)
)[0]
assert correct_game_picks.count() == 1
assert pick.correct_count == 1
💥 2 == 1
😭
In the SQL you generate the subquery inside the COUNT aggregate is joining to a games_game table that isn't anywhere else in your question. It looks like it's doing this to figure out if the pick is correct where elsewhere in your question you have a column on GamePick called is_correct that is used for this.
Here is how you would do it assuming you have the is_correct column and ignoring the games_game table
from django.db.models import Subquery, OuterRef, Count
subquery = GamePick.objects.filter(
pick=OuterRef('id'),
is_correct=True
).values(
'pick_id' # Necessary to get the proper group by
).annotate(
count=Count('pk')
).values(
'id' # Necessary to select only one column
)
picks = Pick.objects.annotate(correct_count=Subquery(subquery))
You can get the same thing using the django-sql-utils package. pip install django-sql-utils and then
from sql_util.utils import SubqueryCount
from django.db.models import Q
subquery = SubqueryCount('game_pick', filter=Q(is_correct=True))
picks=Pick.objects.annotate(correct_count=subquery)
If you need to determine if the pick is correct using the games_game table, I think you would replace is_correct=True (in both examples above) with
game__winning_team_id=F('picked_team_id')
I'm not 100% certain since I can't see those models/columns.
Just got it!
I guess I was making it more complicated than it needed to be.
correct_game_picks = GamePick.objects.filter(
pick=models.OuterRef("pk"),
is_correct=True
)
picks = Pick.objects.annotate(
correct_count=models.Count(
models.Q(game_picks__in=correct_game_picks)
)
)
and the resulting SQL:
SELECT
"picks_pick"."id",
"picks_pick"."picker",
"picks_pick"."pot_id",
COUNT(
"picks_gamepick"."id" IN (
SELECT U0."id" FROM "picks_gamepick" U0
INNER JOIN "games_game" U1 ON (U0."game_id" = U1."id")
WHERE ((U0."picked_team_id" = U1."winning_team_id") AND U0."pick_id" = "picks_pick"."id"))
) AS "correct_count"
FROM "picks_pick"
LEFT OUTER JOIN "picks_gamepick" ON ("picks_pick"."id" = "picks_gamepick"."pick_id")
GROUP BY "picks_pick"."id", "picks_pick"."picker", "picks_pick"."pot_id"
This seemingly unrelated blog post I came a across when searching for "Django subquery count" pointed me in the right direction:
https://mattrobenolt.com/the-django-orm-and-subqueries/
Nope. The above does not work. For some reason it only counts the number of game picks... 🤦♂️
Guess a proper look into the docs is always helpful:
correct_game_picks = GamePick.objects.filter(
is_correct=True,
)
picks = Pick.objects.all().annotate(
correct_count=models.Count(
"game_picks", # The field to count needs to be mentioned specifically
filter=models.Q(game_picks__in=correct_game_picks), # ... and you can define a filter to limit the number of rows in the aggregate
distinct=True. # Prevent duplicates! Important for counting rows
)
)
The aggregate filter is what is was looking for: https://docs.djangoproject.com/en/3.2/ref/models/querysets/#aggregate-filter
This is the generated SQL:
SELECT
"picks_pick"."id",
"picks_pick"."picker",
"picks_pick"."pot_id",
COUNT(
DISTINCT "picks_gamepick"."id"
) FILTER (
WHERE "picks_gamepick"."id" IN (
SELECT U0."id" FROM "picks_gamepick" U0
INNER JOIN "games_game" U1 ON (U0."game_id" = U1."id")
WHERE (U0."picked_team_id" = U1."winning_team_id")
)
)
AS "correct_count"
FROM "picks_pick"
LEFT OUTER JOIN "picks_gamepick" ON ("picks_pick"."id" = "picks_gamepick"."pick_id")
GROUP BY "picks_pick"."id", "picks_pick"."picker", "picks_pick"."pot_id"
I have a model called Location and I'm querying the model with filters that yield 4000 objects:
count = Location.objects.filter(**filters).count()
4000
there is a related Model called KPIs, each Location has many KPIs and there are 2,944,000 KPIs records.
I have a very complex query for the Location that annotates a lot of the KPIs data.
the annotations:
def contribute_annotations(self):
user = self.request.user
self.kpis = user.user_selected_kpis.get_all_kpis_qs()
kpis_names = tuple(kpi.internal_name for kpi in self.kpis)
branch_date = Subquery(BranchKPIs.objects.
filter(branch__location__id=OuterRef(ID)).
order_by('-date').
values(DATE)[:1]
)
# summing the members amount
filters_for_branch = (
Q(location_branches__prem=True) &
~Q(location_branches__branch_scores__members_count=0) &
Q(location_branches__branch_scores__date=F(BRANCH_DATE))
)
sum_of_members_prem_count = Coalesce(Sum('location_branches__branch_scores__members_count',
output_field=IntegerField(),
filter=filters_for_branch),
0)
# location kpis prefetch object
location_kpis_qs = LocationKPIs.objects.filter(date__range=month_range).only(DATE, LOCATION, *kpis_names)
prefetch_location_kpis = Prefetch(lookup=RelatedNames.LOCATION_SCORES,
queryset=location_kpis_qs,
)
assigned_members_count_of_latest = Case(When(location_scores__date=F(LATEST_DATE),
then=f'location_scores__assigned_members_count'))
members_count_of_latest = Case(When(location_scores__date=F(LATEST_DATE),
then=f'location_scores__members_count'))
# kpis annotations for Avg, Trends, and Sizing
kpis_annotations, alias_for_trends, kpis_objects = {}, {}, {}
for kpi in self.kpis:
name = kpi.internal_name
# annotating the last kpi score
kpis_annotations[name] = Case(When(location_scores__date=F('latest_date'),
then=f'location_scores__{name}'), default=0)
# annotating the kpi's month avg
alias_for_trends[f'{name}_avg'] = Coalesce(
Avg(f'location_scores__{name}',
filter=Q(location_scores__date__range=month_range), output_field=IntegerField()
),
0
)
# comparing latest score to the monthly avg in order to determine the kpi's trend
when_equal = When(**{f'{name}_avg': F(name)}, then=0)
when_trend_is_down = When(**{f'{name}_avg__gt': F(name)}, then=-1)
when_trend_is_up = When(**{f'{name}_avg__lt': F(name)}, then=1)
kpi_trend = Case(when_equal, when_trend_is_up, when_trend_is_down,
default=0, output_field=IntegerField())
# annotating the score color
when_red = When(**{f'{name}__gte': kpi.location_level_red_threshold.lower,
f'{name}__lte': kpi.location_level_red_threshold.upper},
then=1
)
when_yellow = When(**{f'{name}__gte': kpi.location_level_yellow_threshold.lower,
f'{name}__lte': kpi.location_level_yellow_threshold.upper},
then=2
)
when_green = When(**{f'{name}__gte': kpi.location_level_green_threshold.lower,
f'{name}__lte': kpi.location_level_green_threshold.upper},
then=3
)
score_type = Case(when_red, when_yellow, when_green, default=2)
# outputs kpi : {score: int, trend: int, score_type: int}
kpis_objects[name] = JSONObject(
score=F(name),
trend=kpi_trend,
score_type=score_type
)
# cases for the pin size of the location, it depends on how many members are in it
when_in_s_size = When(
Q(member_count__gte=settings.S_LOCATION_SIZE[0]) & Q(member_count__lte=settings.S_LOCATION_SIZE[-1]),
then=1)
when_in_m_size = When(
Q(member_count__gte=settings.M_LOCATION_SIZE[0]) & Q(member_count__lte=settings.M_LOCATION_SIZE[-1]),
then=2)
when_in_l_size = When(
Q(member_count__gte=settings.L_LOCATION_SIZE[0]) & Q(member_count__lte=settings.L_LOCATION_SIZE[-1]),
then=3)
when_in_xl_size = When(
Q(member_count__gte=settings.XL_LOCATION_SIZE[0]) & Q(member_count__lte=settings.XL_LOCATION_SIZE[-1]),
then=4)
location_size = Case(when_in_s_size, when_in_m_size, when_in_l_size, when_in_xl_size,
default=2,
output_field=IntegerField())
# location's address string
location_str = Concat(LOCATION__STREET, LOCATION__CITY, LOCATION__COUNTRY,
output_field=CharField())
return (
sum_of_members_prem_count, prefetch_location_kpis, assigned_members_count_of_latest, members_count_of_latest,
kpis_annotations, location_size, alias_for_trends, location_str, kpis_names, kpis_objects, branch_date)
filters = {'user': self.request.user, ACTIVE: True}
(sum_of_members_prem_count, prefetch_location_kpis, assigned_members_count_of_latest, members_count_of_latest,
kpis_annotations, location_size, alias_for_trends, location_str, kpis_names, kpis_objects, branch_date) = self.contribute_annotations()
query_set = (Location.objects.
filter(**filters).
select_related(RelatedNames.LOCATION).
prefetch_related(prefetch_location_kpis).
alias(latest_date=Max('scores__date'),
branch_date=branch_date,
**alias_for_trends,
**kpis_annotations
).
annotate(members_prem_count=sum_of_members_prem,
members_count=members_count_of_latest,
assigned_members_count=assigned_count_of_latest,
farm_latitude=Min(LOCATION__LATITUDE),
farm_longitude=Min(LOCATION__LONGITUDE),
address=location_str,
farm_size=farm_size,
latest_date=Max('farm_scores__date'),
**kpis_objects
).
values(ID, NAME, ADMIN_EMAIL, ADMIN_PHONE, MEMBERS_PREM_COUNT,
MEMBERS_COUNT, ASSIGNED_MEMBERS_COUNT, SIZE, ADDRESS,
latitude=F(LOCATION_LATITUDE), longitude=F(LOCATION_LONGITUDE), *kpis_names
)
)
this query yields 2,944,000 records, which means each for each KPI record and not Location.
I tried adding distinct calls in several ways but I either end up with:
NotImplementedError: annotate() + distinct(fields) is not implemented.
Or the query just ignores it and doesn't add distinct location objects.
the docs suggest that values and distinct don't play nice together and that probably somewhere there is an order by that breaks it.
I've looked at all the involved models, queries and subqueries and removed the order by but it still doesn't work.
I also tried adding this to the query:
query_set.query.clear_ordering(True)
query_set = query_set.order_by(ID).distinct(ID)
but this raises that NotImplementedError
Well, I'm not sure why it's like this and maybe in some cases it won't work.
But, I changed the query to the following:
query_set = (Location.objects.
filter(**filters).
select_related(RelatedNames.LOCATION).
prefetch_related(prefetch_location_kpis).
alias(latest_date=Max('scores__date'),
branch_date=branch_date,
**alias_for_trends,
**kpis_annotations
).
distinct(ID).
annotate(members_prem_count=sum_of_members_prem,
members_count=members_count_of_latest,
assigned_members_count=assigned_count_of_latest,
farm_latitude=Min(LOCATION__LATITUDE),
farm_longitude=Min(LOCATION__LONGITUDE),
address=location_str,
farm_size=farm_size,
latest_date=Max('farm_scores__date'),
**kpis_objects
).
distinct(ID)
)
and overriding Django's source code in django/db/models/sql/compiler.py
line 595
if grouping:
if distinct_fields:
raise NotImplementedError('annotate() + distinct(fields) is not implemented.')
order_by = order_by or self.connection.ops.force_no_ordering()
result.append('GROUP BY %s' % ', '.join(grouping))
if self._meta_ordering:
order_by = None
if having:
result.append('HAVING %s' % having)
params.extend(h_params)
just commented out the if distinct_fields condition
if grouping:
# if distinct_fields:
# raise NotImplementedError('annotate() + distinct(fields) is not implemented.')
order_by = order_by or self.connection.ops.force_no_ordering()
result.append('GROUP BY %s' % ', '.join(grouping))
if self._meta_ordering:
order_by = None
if having:
result.append('HAVING %s' % having)
params.extend(h_params)
I have a form which is collecting data about a variable to be created. I want to create a list of variables which are already there in the database. I am doing this by creating a ManyToMany relationship. When I start the server, the list of variables gets saved on the application, but it does not alter the database field named selective list.
forms.py
class VariableForm(ModelForm):
class Meta:
model = variable
fields = ['name', 'area', 'parameterName', 'order', 'type', 'format', 'units', 'comboItems',
'hiAlarm', 'loAlarm', 'scaleHiMax', 'scaleLoMax', 'deviationAlarm','selectiveList', 'round',
'days', 'required', 'hidden', 'readOnly', 'holdLast', 'calibrationFrequency',
'dateNextCalibration', 'triggerCalibrated']
widgets = {
'comboItems': forms.Textarea(attrs={'rows':1, 'cols': 40, 'style': 'height: 2em;padding-top:0'}),
'forceValue': forms.Textarea(attrs={'rows':1, 'cols': 40, 'style': 'height: 2em;padding-top:0',
'placeholder':'This will force all input to this variable'})
}
#selectiveList = forms.ModelMultipleChoiceField(queryset=variable.objects.all().order_by('-name').reverse())
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(VariableForm, self).__init__(*args, **kwargs)
self.fields['round'] = forms.ModelMultipleChoiceField(
queryset=opRound.objects.all(),
widget=forms.SelectMultiple,
label='Rounds',
required=False
)
self.fields['selectiveList'] = forms.ModelMultipleChoiceField(
queryset=variable.objects.all().order_by('-name').reverse(),
widget=forms.SelectMultiple,
label='Select Variables',
required=False
)
self.fields['days'] = forms.ModelMultipleChoiceField(
queryset=dayOfWeek.objects.all(),
widget=forms.SelectMultiple,
label='Days',
required=False
)
self.fields['area'].choices = AreaIterator(request = self.request)
try:
self.fields['order'].initial = variable.objects.latest().order+1
except Exception,e:
print e
model.py
class variable(models.Model):
name = models.CharField(max_length=255)
area = models.ForeignKey(area)#parent
order = models.IntegerField("Order Index (0 is first, 1 is next, etc.)",default=999)#order index to display in order correctly, ascending
type = models.CharField(max_length=255, choices=(("Value", "Value"), ("Runtime", "Runtime/FlowTotal"),
("Message", "Message"), ("CheckBox", "Check Box List"), ("Selection", "Selection Box"), ("Formula2", "Formula with other Variables"),
("OnOff", "In/Out of Service Selection"), ("OnOffSelection", "Selective On/Off")),
default = "Value" )#what type of variable
format = models.CharField(max_length=255,choices=(("Number", "Number (Without Decimals)"),
("2Number", "Number (With Decimals)"), ("Date", "Date"),
("Time", "Time"), ("Text", "Text")), blank=True, null=True, default="2Number" )#number format if needed
units = models.CharField(max_length=255,blank=True,null=True)#units of measurement
required = models.BooleanField(default=True)#is the variable required in a round
hiAlarm = models.FloatField("High Alarm",blank=True,null=True)#red notify if above
loAlarm = models.FloatField("Low Alarm",blank=True,null=True)#yellow notify if below
scaleHiMax = models.FloatField("Limit maximum value",blank=True,null=True)#scale to high max if needed
scaleLoMax = models.FloatField("Limit low value",blank=True,null=True)#scale to low if needed
deviationAlarm = models.FloatField("Deviation Alarm",blank=True,null=True,
help_text="Triggers an alarm if the value between this record and the last is greater than this percentage.")#%change check
round = models.ManyToManyField(opRound,blank=True)#round of gathering data
days = models.ManyToManyField(dayOfWeek,blank=True)#day of the week
selectiveList = models.ManyToManyField("self",through="variable",blank=True,symmetrical=False)#List to determine which variables to display when the selection is "OFF"
parameterName = models.CharField("Parameter Name (ReportID)",max_length=255,blank=True,null=True)#hachWIM ID
comboItems = models.TextField("List of comma separated options.", blank=True,null=True)#list deliminated by a column for choosing
#PUT EQUATION HERE
hidden = models.BooleanField("Sync to tablet",default=True)#this muse be True if required is true
readOnly = models.BooleanField("Read only.", default = False)
dateTimeEdited = models.DateTimeField(auto_now=True)#date edited
userEdited = models.ForeignKey(CustomUser,blank=True,null=True,related_name="userEditedV")# last user to edit data
forceValue = models.TextField(blank=True,null=True)#force reading
userForced = models.ForeignKey(CustomUser,blank=True,null=True,related_name="userForcedV")# last user to force data
useForce = models.BooleanField("Turn Forcing On", default=False)
dateNextCalibration = models.DateField("Next Calibration Date", blank=True,null=True, help_text="YYYY-MM-DD")
triggerCalibrated = models.BooleanField("Trigger next calibration date", default=False)
calibrationFrequency = models.ForeignKey(calibrationFrequency, blank=True, null=True)
version = models.BigIntegerField(default=1)#used for sync
holdLast = models.BooleanField("Selecting this will hold the last value on the tablet automatically.", default=False)
When we are trying to do it from different models, such as round or days, it creates a new database table with those relations. I want to store the selected values as a string list in the selective list column in the same model.
Here is what the multiple select looks like.
I am trying to create a task list with each task having a datetime attribute. The tasks needs to be in order with t_created being the first and t_paid being last. The order is shown in step_datetime. The description for each tasks is in STEPS.
I currently have two methods all_steps and next_step that shows the task list information. The two methods also need to display the name of the user_created, but that variable won't be defined until the methods are called. That's why I am doing a string replace method.
I feel like I am repeating my code a lot, and I want to follow the DRY principle of Django. Is there any way I could improve this code?
Here is my full code:
class Order( models.Model ) :
def __unicode__( self ) :
return unicode( self.id )
def comments_count( self ) :
return OrderComment.objects.filter( order = self.id ).count()
def all_steps( self ) :
user = self.user_created.first_name
steps = []
step_datetime = [
self.t_created,
self.t_action,
self.t_followup_one,
self.t_vendor_appt_one,
self.t_vendor_appt_two,
self.t_work_done,
self.t_followup_two,
self.t_paid,
]
for ( i, step ) in enumerate( self.STEPS ) :
steps.append( ( step_datetime[ i ], step.replace( '<user_created>', user ), ) )
return steps
def next_step( self ) :
user = self.user_created.first_name
step = 0
if self.t_action is None :
step = 0
elif self.t_followup_one is None :
step = 1
elif self.t_vendor_appt_one is None :
step = 2
elif self.t_vendor_appt_two is None :
step = 3
elif self.t_work_done is None :
step = 4
elif self.t_followup_two is None :
step = 5
elif self.paid is None :
step = 6
return str( step ) + ": " + self.STEPS[ step ].replace( '<user_created>', user )
STEPS = [
"Review, then either approve or reject the order.",
"Follow up with <user_created>",
"Contact the vendor to get a quote and arrange an appointment for <user_created>.",
"Review the quote, (get owner approval), then arrange a second appointment for the repairs.",
"Confirm the finished repairs and pay the vendor.",
"Follow up again with <user_created>",
"Confirm payment and close the order.",
]
ACTION_CHOICES = (
( 'p', 'pending' ),
( 'a', 'approved' ),
( 'r', 'rejected' ),
( 'c', 'closed' ),
)
user_created = models.ForeignKey( User, related_name = 'user_created', verbose_name = 'created by' )
user_action = models.ForeignKey( User, related_name = 'user_status' , verbose_name = 'action by' , null = True, blank = True )
t_created = models.DateTimeField( auto_now_add = True, verbose_name = 'created' )
t_action = models.DateTimeField( null = True, blank = True, verbose_name = 'action' )
t_followup_one = models.DateTimeField( null = True, blank = True, verbose_name = 'first follow-up' )
t_vendor_appt_one = models.DateTimeField( null = True, blank = True, verbose_name = 'first appointment' )
t_vendor_appt_two = models.DateTimeField( null = True, blank = True, verbose_name = 'second appointment' )
t_work_done = models.DateTimeField( null = True, blank = True, verbose_name = 'work done' )
t_followup_two = models.DateTimeField( null = True, blank = True, verbose_name = 'second follow-up' )
t_paid = models.DateTimeField( null = True, blank = True, verbose_name = 'paid' )
action = models.CharField( max_length = 1, choices = ACTION_CHOICES, default = 'p' )
quote = models.DecimalField( max_digits = 8, decimal_places = 2, null = True, blank = True )
payment = models.DecimalField( max_digits = 8, decimal_places = 2, null = True, blank = True )
items = models.ManyToManyField( Item, null = True, blank = True )
t_modified = models.DateTimeField( auto_now = True, verbose_name = 'modified' )
After accepting #Dougal's answer. I changed some of the variables around and came up with this:
def all_steps( self ) :
user = self.user_created.first_name
return [
( getattr( self, attr ), task.format( user = user ) )
for ( attr, task ) in self.TASKS
]
def next_step( self ) :
user = self.user_created.first_name
task_num = next(
( i for ( i, ( attr, task ) ) in enumerate( self.TASKS ) if getattr( self, attr ) is None ),
None
)
if task_num == None :
return "Done!"
else:
return "{number}: {task}".format(
number = str( task_num + 1 ),
task = self.TASKS[ task_num ][ 1 ].format( user = user )
)
TASKS = (
( "t_action" , "Review, then either approve or reject the order." ),
( "t_followup_one" , "Follow up with {user}." ),
( "t_vendor_appt_one", "Contact the vendor to get a quote and arrange an appointment for {user}." ),
( "t_vendor_appt_two", "Review the quote, (get owner approval), then arrange a second appointment for the repairs." ),
( "t_work_done" , "Confirm the finished repairs and pay the vendor." ),
( "t_followup_two" , "Follow up again with {user}." ),
( "t_paid" , "Confirm payment and close the order." ),
)
You can do things like:
for prop in ("t_created", "t_created2" ... ):
val = getattr(self, prop)
# some logic that works with that, maybe uses setattr
Adding on to #Marcin's answer:
You could make a tuple of the property names (say _STEP_NAMES at the module level; you could also make it at the class level, like STEPS, or even just combine the two into a tuple of pairs of attributes and names; that might be a little cleaner). Also, STEPS should probably be a tuple, since it shouldn't be modifiable at runtime.
Then you can reduce your code to:
def all_steps(self):
user = self.user_created.first_name
return [(getattr(self, attr), step.replace('<user_created>', user))
for attr, step in zip(_STEP_NAMES, self.STEPS)]
def next_step(self):
user = self.user_created.first_name
step = next((i for i, attr in enumerate(_STEP_NAMES)
if getattr(self, attr) is None),
None) # assumes Python 2.6+
if step == None:
return "Done!"
else:
return str(step) + ": " + self.STEPS[step].replace('<user_created>', user)
If you need Python 2.4/2.5 compatability, the next line can be replaced by
try:
step = (i for i, attr in enumerate(_STEP_NAMES) if getattr(self, attr) is None).next()
except StopIteration:
return "Done!"
return str(step) + ": " + self.STEPS[step].replace('<user_created>', user)