update width-calculation for bar-charts
[mygpo.git] / mygpo / publisher / utils.py
blob295181eb183f23c231cc858856f0b279e570ba4a
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 from datetime import timedelta, date
19 from mygpo.utils import daterange
20 from mygpo.api.models import Episode, EpisodeAction
21 from mygpo.data.models import HistoricPodcastData
22 from mygpo.web.utils import flatten_intervals
23 from mygpo.publisher.models import PodcastPublisher
24 from mygpo.api.constants import DEVICE_TYPES
25 from django.db.models import Avg
26 from django.contrib.auth.models import User
29 def listener_data(podcasts):
30 day = timedelta(1)
32 # get start date
33 d = date(2010, 1, 1)
34 episode_actions = EpisodeAction.objects.filter(episode__podcast__in=podcasts, timestamp__gte=d, action='play').order_by('timestamp').values('timestamp')
35 if len(episode_actions) == 0:
36 return []
38 start = episode_actions[0]['timestamp']
40 # pre-calculate episode list, make it index-able by release-date
41 episodes = {}
42 for episode in Episode.objects.filter(podcast__in=podcasts):
43 if episode.timestamp:
44 episodes[episode.timestamp.date()] = episode
46 days = []
47 for d in daterange(start):
48 next = d + timedelta(days=1)
49 listener_sum = 0
51 # this is faster than .filter(episode__podcast__in=podcasts)
52 for p in podcasts:
53 listeners = EpisodeAction.objects.filter(episode__podcast=p, timestamp__gte=d, timestamp__lt=next, action='play').values('user_id').distinct().count()
54 listener_sum += listeners
56 if d.date() in episodes:
57 episode = episodes[d.date()]
58 else:
59 episode = None
61 days.append({
62 'date': d,
63 'listeners': listener_sum,
64 'episode': episode})
66 return days
69 def episode_listener_data(episode):
70 d = date(2010, 1, 1)
71 leap = timedelta(days=1)
73 episodes = EpisodeAction.objects.filter(episode=episode, timestamp__gte=d).order_by('timestamp').values('timestamp')
74 if len(episodes) == 0:
75 return []
77 start = episodes[0]['timestamp']
79 intervals = []
80 for d in daterange(start, leap=leap):
81 next = d + leap
82 listeners = EpisodeAction.objects.filter(episode=episode, timestamp__gte=d, timestamp__lt=next).values('user_id').distinct().count()
83 e = episode if episode.timestamp and episode.timestamp >= d and episode.timestamp <= next else None
84 intervals.append({
85 'date': d,
86 'listeners': listeners,
87 'episode': e})
89 return intervals
92 def subscriber_data(podcasts):
93 data = {}
95 #this is fater than a subquery
96 records = []
97 for p in podcasts:
98 records.extend(HistoricPodcastData.objects.filter(podcast=p).order_by('date'))
100 for r in records:
101 if r.date.day == 1:
102 s = r.date.strftime('%y-%m')
103 val = data.get(s, 0)
104 data[s] = val + r.subscriber_count
106 list = []
107 for k, v in data.iteritems():
108 list.append({'x': k, 'y': v})
110 list.sort(key=lambda x: x['x'])
112 return list
115 def check_publisher_permission(user, podcast):
116 if user.is_staff:
117 return True
119 if PodcastPublisher.objects.filter(user=user, podcast=podcast).count() > 0:
120 return True
122 return False
124 def episode_list(podcast):
125 episodes = Episode.objects.filter(podcast=podcast).order_by('-timestamp')
126 for e in episodes:
127 listeners = EpisodeAction.objects.filter(episode=e, action='play').values('user').distinct()
128 e.listeners = listeners.count()
130 return episodes
133 def device_stats(podcasts):
134 res = {}
135 for type in DEVICE_TYPES:
136 c = 0
138 # this is faster than a subquery
139 for p in podcasts:
140 c += EpisodeAction.objects.filter(episode__podcast=p, device__type=type[0]).values('user_id').distinct().count()
141 if c > 0:
142 res[type[1]] = c
144 return res
147 def episode_heatmap(episode, max_part_num=50, min_part_length=10):
149 Generates "Heatmap Data" for the given episode
151 The episode is split up in parts having max 'max_part_num' segments which
152 are all of the same length, minimum 'min_part_length' seconds.
154 For each segment, the number of users that have played it (at least
155 partially) is calculated and returned
158 episode_actions = EpisodeAction.objects.filter(episode=episode, action='play')
160 if episode.duration:
161 duration = episode.duration
162 else:
163 duration = episode_actions.aggregate(duration=Avg('total'))['duration']
165 if not duration:
166 return [0], 0
168 part_length = max(min_part_length, int(duration / max_part_num))
170 part_num = int(duration / part_length)
172 heatmap = [0]*part_num
174 user_ids = [x['user'] for x in episode_actions.values('user').distinct()]
175 for user_id in user_ids:
176 user = User.objects.get(id=user_id)
177 actions = episode_actions.filter(user=user, playmark__isnull=False, started__isnull=False)
178 if actions.exists():
179 played_parts = flatten_intervals(actions)
180 user_heatmap = played_parts_to_heatmap(played_parts, part_length, part_num)
181 heatmap = [sum(pair) for pair in zip(heatmap, user_heatmap)]
183 return heatmap, part_length
186 def played_parts_to_heatmap(played_parts, part_length, part_count):
188 takes the (flattened) parts of an episode that a user has played, and
189 generates a heatmap data for this user.
191 The result is a list with part_count elements, each having a value
192 of either 0 (user has not played that part) or 1 (user has at least
193 partially played that part)
195 parts = [0]*part_count
197 if not played_parts:
198 return parts
200 part_iter = iter(played_parts)
201 current_part = part_iter.next()
203 for i in range(0, part_count):
204 part = i * part_length
205 while current_part['end'] < part:
206 try:
207 current_part = part_iter.next()
208 except StopIteration:
209 return parts
211 if current_part['start'] <= (part + part_length) and current_part['end'] >= part:
212 parts[i] = 1
213 return parts
216 def colour_repr(val, max_val, colours):
218 returns a color representing the given value within a color gradient.
220 The color gradient is given by a list of (r, g, b) tupels. The value
221 is first located within two colors (of the list) and then approximated
222 between these two colors, based on its position within this segment.
224 if len(colours) == 1:
225 return colours[0]
227 # calculate position in the gradient; defines the segment
228 pos = float(val) / max_val
229 colour_nr1 = min(len(colours)-1, int(pos * (len(colours)-1)))
230 colour_nr2 = min(len(colours)-1, colour_nr1+1)
231 colour1 = colours[ colour_nr1 ]
232 colour2 = colours[ colour_nr2 ]
234 r1, g1, b1 = colour1
235 r2, g2, b2 = colour2
237 # determine bounds of segment
238 lower_bound = float(max_val) / (len(colours)-1) * colour_nr1
239 upper_bound = min(max_val, lower_bound + float(max_val) / (len(colours)-1))
241 # position within the segment
242 percent = (val - lower_bound) / upper_bound
244 r_step = r2 - r1
245 g_step = g2 - g1
246 b_step = b2 - b1
248 return (r1 + r_step * percent, g1 + g_step * percent, b1 + b_step * percent)