reduce queries to Database URL
[mygpo.git] / mygpo / core / slugs.py
blob35f59628946705ee1d3729561108639a764b3717
1 from itertools import count
3 from couchdbkit.ext.django.schema import *
5 from django.template.defaultfilters import slugify
7 from mygpo.utils import multi_request_view
8 from mygpo.decorators import repeat_on_conflict
11 def assign_slug(obj, generator):
12 if obj.slug:
13 return
15 slug = generator(obj).get_slug()
16 _set_slug(obj=obj, slug=slug)
19 def assign_missing_episode_slugs(podcast):
20 common_title = podcast.get_common_episode_title()
22 episodes = EpisodesMissingSlugs(podcast.get_id())
25 for episode in episodes:
26 slug = EpisodeSlug(episode, common_title).get_slug()
27 _set_slug(obj=episode, slug=slug)
30 @repeat_on_conflict(['obj'])
31 def _set_slug(obj, slug):
32 if slug:
33 obj.set_slug(slug)
34 obj.save()
38 class SlugGenerator(object):
39 """ Generates a unique slug for an object """
42 def __init__(self, obj):
43 if obj.slug:
44 raise ValueError('%(obj)s already has slug %(slug)s' % \
45 dict(obj=obj, slug=obj.slug))
47 self.base_slug = self._get_base_slug(obj)
50 @staticmethod
51 def _get_base_slug(obj):
52 if not obj.title:
53 return None
54 base_slug = slugify(obj.title)
55 return base_slug
58 @staticmethod
59 def _get_existing_slugs():
60 return []
63 def get_slug(self):
64 """ Gets existing slugs and appends numbers until slug is unique """
65 if not self.base_slug:
66 return None
68 existing_slugs = self._get_existing_slugs()
70 if not self.base_slug in existing_slugs:
71 return str(self.base_slug)
73 for n in count(1):
74 tmp_slug = '%s-%d' % (self.base_slug, n)
75 if not tmp_slug in existing_slugs:
76 # slugify returns SafeUnicode, we need a plain string
77 return str(tmp_slug)
81 class PodcastGroupSlug(SlugGenerator):
82 """ Generates slugs for Podcast Groups """
84 def _get_existing_slugs(self):
85 from mygpo.core.models import Podcast
87 res = Podcast.view('podcasts/by_slug',
88 startkey = [self.base_slug, None],
89 endkey = [self.base_slug + 'ZZZZZ', None],
90 wrap_doc = False,
92 return [r['key'][0] for r in res]
96 class PodcastSlug(PodcastGroupSlug):
97 """ Generates slugs for Podcasts """
99 @staticmethod
100 def _get_base_slug(podcast):
101 base_slug = SlugGenerator._get_base_slug(podcast)
103 if not base_slug:
104 return None
106 # append group_member_name to slug
107 if podcast.group_member_name:
108 member_slug = slugify(podcast.group_member_name)
109 if member_slug and not member_slug in base_slug:
110 base_slug = '%s-%s' % (base_slug, member_slug)
112 return base_slug
116 class EpisodeSlug(SlugGenerator):
117 """ Generates slugs for Episodes """
119 def __init__(self, episode, common_title):
120 self.common_title = common_title
121 super(EpisodeSlug, self).__init__(episode)
122 self.podcast_id = episode.podcast
125 def _get_base_slug(self, obj):
127 number = obj.get_episode_number(self.common_title)
128 if number:
129 return str(number)
131 short_title = obj.get_short_title(self.common_title)
132 if short_title:
133 return slugify(short_title)
135 if obj.title:
136 return slugify(obj.title)
138 return None
141 def _get_existing_slugs(self):
142 """ Episode slugs have to be unique within the Podcast """
143 from mygpo.core.models import Episode
145 res = Episode.view('episodes/by_slug',
146 startkey = [self.podcast_id, self.base_slug],
147 endkey = [self.podcast_id, self.base_slug + 'ZZZZZ'],
148 wrap_doc = False,
150 return [r['key'][1] for r in res]
153 class ObjectsMissingSlugs(object):
154 """ A collections of objects missing a slug """
156 def __init__(self, cls, wrapper=None, start=[None], end=[{}]):
157 self.cls = cls
158 self.doc_type = cls._doc_type
159 self.wrapper = wrapper
160 self.start = start
161 self.end = end
162 self.kwargs = {}
164 def __len__(self):
165 res = self.cls.view('slugs/missing',
166 startkey = [self.doc_type] + self.end,
167 endkey = [self.doc_type] + self.start,
168 descending = True,
169 reduce = True,
170 group = True,
171 group_level = 1,
173 return res.first()['value'] if res else 0
176 def __iter__(self):
178 return multi_request_view(self.cls, 'slugs/missing',
179 startkey = [self.doc_type] + self.end,
180 endkey = [self.doc_type] + self.start,
181 descending = True,
182 include_docs = True,
183 reduce = False,
184 wrapper = self.wrapper,
185 auto_advance = False,
186 **self.kwargs
190 class PodcastsMissingSlugs(ObjectsMissingSlugs):
191 """ Podcasts that don't have a slug (but could have one) """
193 def __init__(self):
194 from mygpo.core.models import Podcast
195 super(PodcastsMissingSlugs, self).__init__(Podcast, self._podcast_wrapper)
196 self.kwargs = {'wrap': False}
198 @staticmethod
199 def _podcast_wrapper(r):
200 from mygpo.core.models import Podcast, PodcastGroup
202 doc = r['doc']
204 if doc['doc_type'] == 'Podcast':
205 return Podcast.wrap(doc)
206 else:
207 pid = r['key'][2]
208 pg = PodcastGroup.wrap(doc)
209 return pg.get_podcast_by_id(pid)
211 def __iter__(self):
212 for r in super(PodcastsMissingSlugs, self).__iter__():
213 yield self._podcast_wrapper(r)
216 class EpisodesMissingSlugs(ObjectsMissingSlugs):
217 """ Episodes that don't have a slug (but could have one) """
219 def __init__(self, podcast_id=None):
220 from mygpo.core.models import Episode
222 if podcast_id:
223 start = [podcast_id, None]
224 end = [podcast_id, {}]
225 else:
226 start = [None, None]
227 end = [{}, {}]
229 super(EpisodesMissingSlugs, self).__init__(Episode,
230 self._episode_wrapper, start, end)
232 @staticmethod
233 def _episode_wrapper(doc):
234 from mygpo.core.models import Episode
236 return Episode.wrap(doc)
239 class PodcastGroupsMissingSlugs(ObjectsMissingSlugs):
240 """ Podcast Groups that don't have a slug (but could have one) """
242 def __init__(self):
243 from mygpo.core.models import PodcastGroup
244 super(PodcastGroupsMissingSlugs, self).__init__(PodcastGroup,
245 self._group_wrapper)
247 @staticmethod
248 def _group_wrapper(doc):
249 from mygpo.core.models import PodcastGroup
250 return PodcastGroup.wrap(doc)
253 class SlugMixin(DocumentSchema):
254 slug = StringProperty()
255 merged_slugs = StringListProperty()
257 def set_slug(self, slug):
258 """ Set the main slug of the Podcast """
260 if not isinstance(slug, basestring):
261 raise ValueError('slug must be a string')
263 if not slug:
264 raise ValueError('slug cannot be empty')
266 if self.slug:
267 self.merged_slugs.append(self.slug)
269 self.slug = slug