mv tags/by_user to usertags/by_user to userdata db
[mygpo.git] / mygpo / core / slugs.py
blob0d3d9052f6f3cddbcc36d684879059cc6d2ca958
1 from collections import defaultdict
3 from itertools import count
5 from couchdbkit.ext.django.schema import *
7 from django.utils.text import slugify
9 from mygpo.decorators import repeat_on_conflict
10 from mygpo.utils import partition
13 def assign_slug(obj, generator):
14 if obj.slug:
15 return
17 slug = generator(obj).get_slug()
18 _set_slug(obj=obj, slug=slug)
21 def assign_missing_episode_slugs(podcast):
22 common_title = podcast.get_common_episode_title()
24 episodes = EpisodesMissingSlugs(podcast.get_id())
27 for episode in episodes:
28 slug = EpisodeSlug(episode, common_title).get_slug()
29 _set_slug(obj=episode, slug=slug)
32 @repeat_on_conflict(['obj'])
33 def _set_slug(obj, slug):
34 if slug:
35 obj.set_slug(slug)
36 obj.save()
40 class SlugGenerator(object):
41 """ Generates a unique slug for an object """
44 def __init__(self, obj, override_existing=False):
45 if obj.slug and not override_existing:
46 raise ValueError('%(obj)s already has slug %(slug)s' % \
47 dict(obj=obj, slug=obj.slug))
49 self.base_slug = self._get_base_slug(obj)
52 @staticmethod
53 def _get_base_slug(obj):
54 if not obj.title:
55 return None
56 base_slug = slugify(obj.title)
57 return base_slug
60 @staticmethod
61 def _get_existing_slugs():
62 return []
65 def get_slug(self):
66 """ Gets existing slugs and appends numbers until slug is unique """
67 if not self.base_slug:
68 return None
70 existing_slugs = self._get_existing_slugs()
72 if not self.base_slug in existing_slugs:
73 return str(self.base_slug)
75 for n in count(1):
76 tmp_slug = '%s-%d' % (self.base_slug, n)
77 if not tmp_slug in existing_slugs:
78 # slugify returns SafeUnicode, we need a plain string
79 return str(tmp_slug)
83 class PodcastGroupSlug(SlugGenerator):
84 """ Generates slugs for Podcast Groups """
86 def _get_existing_slugs(self):
87 from mygpo.db.couchdb.podcast import podcast_slugs
88 return podcast_slugs(self.base_slug)
92 class PodcastSlug(PodcastGroupSlug):
93 """ Generates slugs for Podcasts """
95 @staticmethod
96 def _get_base_slug(podcast):
97 base_slug = SlugGenerator._get_base_slug(podcast)
99 if not base_slug:
100 return None
102 # append group_member_name to slug
103 if podcast.group_member_name:
104 member_slug = slugify(podcast.group_member_name)
105 if member_slug and not member_slug in base_slug:
106 base_slug = '%s-%s' % (base_slug, member_slug)
108 return base_slug
112 class EpisodeSlug(SlugGenerator):
113 """ Generates slugs for Episodes """
115 def __init__(self, episode, common_title, override_existing=False):
116 self.common_title = common_title
117 super(EpisodeSlug, self).__init__(episode, override_existing)
118 self.podcast_id = episode.podcast
121 def _get_base_slug(self, obj):
123 number = obj.get_episode_number(self.common_title)
124 if number:
125 return str(number)
127 short_title = obj.get_short_title(self.common_title)
128 if short_title:
129 return slugify(short_title)
131 if obj.title:
132 return slugify(obj.title)
134 return None
137 def _get_existing_slugs(self):
138 """ Episode slugs have to be unique within the Podcast """
139 from mygpo.db.couchdb.episode import episode_slugs_per_podcast
140 return episode_slugs_per_podcast(self.podcast_id, self.base_slug)
143 class ObjectsMissingSlugs(object):
144 """ A collections of objects missing a slug """
146 def __init__(self, cls, wrapper=None, start=[None], end=[{}]):
147 self.cls = cls
148 self.doc_type = cls._doc_type
149 self.wrapper = wrapper
150 self.start = start
151 self.end = end
152 self.kwargs = {}
155 def __len__(self):
156 from mygpo.db.couchdb.common import missing_slug_count
157 return missing_slug_count(self.doc_type, self.start, self.end)
160 def __iter__(self):
161 from mygpo.db.couchdb.common import missing_slugs
162 return missing_slugs(self.doc_type, self.start, self.end, self.wrapper, **self.kwargs)
166 class PodcastsMissingSlugs(ObjectsMissingSlugs):
167 """ Podcasts that don't have a slug (but could have one) """
169 def __init__(self):
170 from mygpo.core.models import Podcast
171 super(PodcastsMissingSlugs, self).__init__(Podcast, self._podcast_wrapper)
172 self.kwargs = {'wrap': False}
174 @staticmethod
175 def _podcast_wrapper(r):
176 from mygpo.core.models import Podcast, PodcastGroup
178 doc = r['doc']
180 if doc['doc_type'] == 'Podcast':
181 return Podcast.wrap(doc)
182 else:
183 pid = r['key'][2]
184 pg = PodcastGroup.wrap(doc)
185 return pg.get_podcast_by_id(pid)
187 def __iter__(self):
188 for r in super(PodcastsMissingSlugs, self).__iter__():
189 yield self._podcast_wrapper(r)
192 class EpisodesMissingSlugs(ObjectsMissingSlugs):
193 """ Episodes that don't have a slug (but could have one) """
195 def __init__(self, podcast_id=None):
196 from mygpo.core.models import Episode
198 if podcast_id:
199 start = [podcast_id, None]
200 end = [podcast_id, {}]
201 else:
202 start = [None, None]
203 end = [{}, {}]
205 super(EpisodesMissingSlugs, self).__init__(Episode,
206 self._episode_wrapper, start, end)
208 @staticmethod
209 def _episode_wrapper(doc):
210 from mygpo.core.models import Episode
212 return Episode.wrap(doc)
215 class PodcastGroupsMissingSlugs(ObjectsMissingSlugs):
216 """ Podcast Groups that don't have a slug (but could have one) """
218 def __init__(self):
219 from mygpo.core.models import PodcastGroup
220 super(PodcastGroupsMissingSlugs, self).__init__(PodcastGroup,
221 self._group_wrapper)
223 @staticmethod
224 def _group_wrapper(doc):
225 from mygpo.core.models import PodcastGroup
226 return PodcastGroup.wrap(doc)
229 class SlugMixin(DocumentSchema):
230 slug = StringProperty()
231 merged_slugs = StringListProperty()
233 def set_slug(self, slug):
234 """ Set the main slug of the object """
236 if self.slug:
237 self.merged_slugs.append(self.slug)
239 self.merged_slugs = list(set(self.merged_slugs) - set([slug]))
241 self.slug = slug
244 def remove_slug(self, slug):
245 """ Removes the slug from the object """
247 # remove main slug
248 if self.slug == slug:
249 self.slug = None
251 # remove from merged slugs
252 self.merged_slugs = list(set(self.merged_slugs) - set([slug]))
255 def get_duplicate_slugs(episodes):
256 """ Finds duplicate slugs and yields (slug, duplicates) pairs for each slug
258 Such a pair is only yielded for each slug that actually has a duplicate.
259 The "duplicates" list does not contain the selected "winner" of a set of
260 duplicates. """
262 # we build a dict of {slug: [episode1, episode2, ...], ...}
263 # for each slug all episodes are given that use this slug
264 slugs = defaultdict(list)
266 for episode in episodes:
267 all_slugs = filter(None, [episode.slug] + episode.merged_slugs)
268 for slug in all_slugs:
269 slugs[slug].append(episode)
271 # filter out unique slugs
272 dups = {s: eps for (s, eps) in slugs.items() if len(eps) > 1}
274 for slug, episodes in dups.items():
275 merged, main = partition(episodes, lambda e: e.slug == slug)
277 main, merged = list(main), list(merged)
279 # we want to determine exactly one winner, the rest is in "merged"
280 if len(main) == 1:
281 winner = main[0]
283 if len(main) < 1:
284 winner = merged.pop()
286 if len(main) > 1:
287 winner, merged = main[0], main[1:] + merged
289 # for every loser, remove the slug
290 yield slug, merged