62 |
62 |
63 @classmethod |
63 @classmethod |
64 def cache_key (self, shorturl) : |
64 def cache_key (self, shorturl) : |
65 return 'qrurls/url/{shorturl}'.format(shorturl=shorturl) |
65 return 'qrurls/url/{shorturl}'.format(shorturl=shorturl) |
66 |
66 |
67 def qrcode_img (self, size=512) : |
|
68 return QRCODE_API.format( |
|
69 width=size, height=size, |
|
70 url=django.utils.http.urlquote(self.qrcode_url()), |
|
71 ) |
|
72 |
|
73 def qrcode_url (self) : |
|
74 return 'HTTP://{domain}{url}'.format( |
|
75 domain = get_current_site(None).domain.upper(), |
|
76 url = self.get_absolute_url(), |
|
77 ) |
|
78 |
|
79 def get_absolute_url (self) : |
|
80 return reverse('shorturl', args=[self.shorturl]) |
|
81 |
|
82 def now (self, now=None) : |
|
83 """ |
|
84 Return database-compatible concept of "now". |
|
85 |
|
86 All datetimes are strictly stored and compared as UTC. Any |
|
87 timezone-aware logic should happen in the admin. |
|
88 """ |
|
89 if now : |
|
90 return now |
|
91 else : |
|
92 return timezone.now() |
|
93 |
|
94 def active_item (self, now=None) : |
|
95 """Currently published URLItem.""" |
|
96 now = self.now(now) |
|
97 |
|
98 try : |
|
99 return self.urlitem_set.filter(published__lt=now).order_by('-published')[0] |
|
100 except IndexError : |
|
101 return None |
|
102 |
|
103 def upcoming_item (self, now=None) : |
|
104 """Next-up to-be-published URLItem.""" |
|
105 now = self.now(now) |
|
106 |
|
107 try : |
|
108 return self.urlitem_set.filter(published__gt=now).order_by('published')[0] |
|
109 except IndexError : |
|
110 return None |
|
111 |
|
112 def last_item (self) : |
|
113 """The last URLItem available.""" |
|
114 |
|
115 try : |
|
116 return self.urlitem_set.order_by('-published')[0] |
|
117 except IndexError : |
|
118 return None |
|
119 |
|
120 @property |
|
121 def publishing_timetz (self) : |
|
122 """publishing_time, with tzinfo on the correct timezone.""" |
|
123 return self.publishing_time.replace(tzinfo=timezone.get_current_timezone()) |
|
124 |
|
125 @property |
|
126 def publishing_offset (self) : |
|
127 return datetime.timedelta(days=self.publishing_days) |
|
128 |
|
129 def publishing_schedule (self) : |
|
130 """Calculate initial URLItem.published values for feed.""" |
|
131 |
|
132 # following the last item in the queue |
|
133 item = self.last_item() |
|
134 |
|
135 if item and item.published > self.now(): |
|
136 # starting from the following day |
|
137 date = item.published.date() + self.publishing_offset |
|
138 else : |
|
139 # defaults to today |
|
140 date = self.now().date() |
|
141 |
|
142 return date, self.publishing_timetz, self.publishing_offset |
|
143 |
|
144 @classmethod |
|
145 def apply_publishing_schedule (self, date, time, offset, count) : |
|
146 """Yield publishing times off given date/time to offset * count.""" |
|
147 for index in xrange(0, count) : |
|
148 yield datetime.datetime.combine(date + offset * index, time) |
|
149 |
|
150 def __unicode__ (self) : |
|
151 return self.shorturl |
|
152 |
|
153 class URLImage(models.Model): |
|
154 image = models.ImageField(upload_to=IMAGES_MEDIA, storage=SecretFileSystemStorage()) |
|
155 name = models.CharField(max_length=512, blank=False) |
|
156 title = models.CharField(max_length=1024, blank=True) |
|
157 uploaded = models.DateTimeField(auto_now_add=True) |
|
158 |
|
159 class Meta: |
|
160 verbose_name = u"URL Image" |
|
161 verbose_name_plural = u"URL Images" |
|
162 ordering = ['uploaded'] |
|
163 |
|
164 def save (self) : |
|
165 # keep real filename before saving with hash |
|
166 # but not when updating! |
|
167 if not self.name: |
|
168 self.name = self.image.name |
|
169 |
|
170 super(URLImage, self).save() |
|
171 |
|
172 def get_absolute_url (self) : |
|
173 return self.image.url |
|
174 |
|
175 def __unicode__ (self) : |
|
176 return "[%s] %s" % (self.uploaded.strftime("%Y-%m-%d"), self.name) |
|
177 |
|
178 class URLItem(models.Model): |
|
179 shorturl = models.ForeignKey(URL) |
|
180 published = models.DateTimeField(db_index=True) # UTC |
|
181 |
|
182 # either-or |
|
183 url = models.URLField(blank=True) # populated from image |
|
184 image = models.ForeignKey(URLImage, null=True, blank=True) |
|
185 |
|
186 class Meta: |
|
187 verbose_name = u"URL Item" |
|
188 verbose_name_plural = u"URL Items" |
|
189 ordering = ['published'] |
|
190 |
|
191 @classmethod |
|
192 def cache_key (cls, shorturl, item_id) : |
|
193 return 'qrurls/url/{shorturl}/{item}'.format(shorturl=shorturl, item=item_id) |
|
194 |
|
195 @classmethod |
|
196 def get (cls, shorturl, item_id=None, related=()) : |
|
197 """ |
|
198 Return the URLItem for a given shorturl, either the given specific one, |
|
199 or the latest, from the database. |
|
200 |
|
201 Raises URLItem.NotFound |
|
202 """ |
|
203 # JOIN against shorturl, urlimage |
|
204 url_item = cls.objects.select_related(*related) |
|
205 |
|
206 if not shorturl: |
|
207 raise cls.DoesNotExist() |
|
208 elif shorturl.isdigit(): |
|
209 shorturl_id = int(shorturl) |
|
210 url_item = url_item.filter(shorturl__id=shorturl_id) |
|
211 else: |
|
212 url_item = url_item.filter(shorturl__shorturl=shorturl) |
|
213 |
|
214 # match for published items |
|
215 now = timezone.now() |
|
216 url_item = url_item.filter(published__lt=now).order_by('-published') |
|
217 |
|
218 if item_id : |
|
219 # specific, but still the published on |
|
220 log.debug("search @ %d", item_id) |
|
221 |
|
222 return url_item.get(id=item_id) # raises DoesNotExist |
|
223 else : |
|
224 # most recent |
|
225 log.debug("search @ %s", now) |
|
226 try: |
|
227 return url_item[0] |
|
228 except IndexError: |
|
229 raise cls.DoesNotExist() |
|
230 |
|
231 @classmethod |
67 @classmethod |
232 def get_url (cls, shorturl) : |
68 def get_url (cls, shorturl) : |
233 """ |
69 """ |
234 Return the current URL for a given shorturl, from cache or DB. |
70 Return the current URL for a given shorturl, from cache or DB. |
235 |
71 |
236 Returns url:str, modified:datetime |
72 Returns url:str, modified:datetime |
237 Raises URLItem.NotFound |
73 Raises URLItem.DoesNotExist |
238 """ |
74 """ |
239 key = URL.cache_key(shorturl) |
75 key = cls.cache_key(shorturl) |
240 get = cache.get(key) |
76 get = cache.get(key) |
241 |
77 |
242 if get : |
78 if get : |
243 log.debug("get cache: %s", key) |
79 log.debug("get cache: %s", key) |
244 return get |
80 return get |
245 else: |
81 else: |
246 # from db |
82 # from db |
247 url_item = cls.get(shorturl=shorturl) |
83 url_item = URLItem.get(shorturl=shorturl) |
248 set = ( |
84 set = ( |
249 url_item.get_absolute_url(), |
85 url_item.get_absolute_url(), |
250 url_item.last_modified() |
86 url_item.last_modified() |
251 ) |
87 ) |
252 |
88 |
253 log.debug("set cache: %s", key) |
89 log.debug("set cache: %s", key) |
254 cache.set(key, set) |
90 cache.set(key, set) |
255 return set |
91 return set |
256 |
92 |
|
93 |
|
94 def qrcode_img (self, size=512) : |
|
95 return QRCODE_API.format( |
|
96 width=size, height=size, |
|
97 url=django.utils.http.urlquote(self.qrcode_url()), |
|
98 ) |
|
99 |
|
100 def qrcode_url (self) : |
|
101 return 'HTTP://{domain}{url}'.format( |
|
102 domain = get_current_site(None).domain.upper(), |
|
103 url = self.get_absolute_url(), |
|
104 ) |
|
105 |
|
106 def get_absolute_url (self) : |
|
107 return reverse('shorturl', args=[self.shorturl]) |
|
108 |
|
109 def now (self, now=None) : |
|
110 """ |
|
111 Return database-compatible concept of "now". |
|
112 |
|
113 All datetimes are strictly stored and compared as UTC. Any |
|
114 timezone-aware logic should happen in the admin. |
|
115 """ |
|
116 if now : |
|
117 return now |
|
118 else : |
|
119 return timezone.now() |
|
120 |
|
121 def active_item (self, now=None) : |
|
122 """Currently published URLItem.""" |
|
123 now = self.now(now) |
|
124 |
|
125 try : |
|
126 return self.urlitem_set.filter(published__lt=now).order_by('-published')[0] |
|
127 except IndexError : |
|
128 return None |
|
129 |
|
130 def upcoming_item (self, now=None) : |
|
131 """Next-up to-be-published URLItem.""" |
|
132 now = self.now(now) |
|
133 |
|
134 try : |
|
135 return self.urlitem_set.filter(published__gt=now).order_by('published')[0] |
|
136 except IndexError : |
|
137 return None |
|
138 |
|
139 def last_item (self) : |
|
140 """The last URLItem available.""" |
|
141 |
|
142 try : |
|
143 return self.urlitem_set.order_by('-published')[0] |
|
144 except IndexError : |
|
145 return None |
|
146 |
|
147 @property |
|
148 def publishing_timetz (self) : |
|
149 """publishing_time, with tzinfo on the correct timezone.""" |
|
150 return self.publishing_time.replace(tzinfo=timezone.get_current_timezone()) |
|
151 |
|
152 @property |
|
153 def publishing_offset (self) : |
|
154 return datetime.timedelta(days=self.publishing_days) |
|
155 |
|
156 def publishing_schedule (self) : |
|
157 """Calculate initial URLItem.published values for feed.""" |
|
158 |
|
159 # following the last item in the queue |
|
160 item = self.last_item() |
|
161 |
|
162 if item and item.published > self.now(): |
|
163 # starting from the following day |
|
164 date = item.published.date() + self.publishing_offset |
|
165 else : |
|
166 # defaults to today |
|
167 date = self.now().date() |
|
168 |
|
169 return date, self.publishing_timetz, self.publishing_offset |
|
170 |
|
171 @classmethod |
|
172 def apply_publishing_schedule (self, date, time, offset, count) : |
|
173 """Yield publishing times off given date/time to offset * count.""" |
|
174 for index in xrange(0, count) : |
|
175 yield datetime.datetime.combine(date + offset * index, time) |
|
176 |
|
177 def __unicode__ (self) : |
|
178 return self.shorturl |
|
179 |
|
180 class URLImage(models.Model): |
|
181 image = models.ImageField(upload_to=IMAGES_MEDIA, storage=SecretFileSystemStorage()) |
|
182 name = models.CharField(max_length=512, blank=False) |
|
183 title = models.CharField(max_length=1024, blank=True) |
|
184 uploaded = models.DateTimeField(auto_now_add=True) |
|
185 |
|
186 class Meta: |
|
187 verbose_name = u"URL Image" |
|
188 verbose_name_plural = u"URL Images" |
|
189 ordering = ['uploaded'] |
|
190 |
|
191 def save (self) : |
|
192 # keep real filename before saving with hash |
|
193 # but not when updating! |
|
194 if not self.name: |
|
195 self.name = self.image.name |
|
196 |
|
197 super(URLImage, self).save() |
|
198 |
|
199 def get_absolute_url (self) : |
|
200 return self.image.url |
|
201 |
|
202 def __unicode__ (self) : |
|
203 return "[%s] %s" % (self.uploaded.strftime("%Y-%m-%d"), self.name) |
|
204 |
|
205 class URLItem(models.Model): |
|
206 shorturl = models.ForeignKey(URL) |
|
207 published = models.DateTimeField(db_index=True) # UTC |
|
208 |
|
209 # either-or |
|
210 url = models.URLField(blank=True) # populated from image |
|
211 image = models.ForeignKey(URLImage, null=True, blank=True) |
|
212 |
|
213 class Meta: |
|
214 verbose_name = u"URL Item" |
|
215 verbose_name_plural = u"URL Items" |
|
216 ordering = ['published'] |
|
217 |
|
218 @classmethod |
|
219 def get (cls, shorturl, item_id=None, related=()) : |
|
220 """ |
|
221 Return the URLItem for a given shorturl, either the given specific one, |
|
222 or the latest, from the database in one SQL query. |
|
223 |
|
224 Raises URLItem.DoesNotExist |
|
225 """ |
|
226 # JOIN against shorturl, urlimage |
|
227 url_item = cls.objects.select_related(*related) |
|
228 |
|
229 if not shorturl: |
|
230 raise cls.DoesNotExist() |
|
231 elif shorturl.isdigit(): |
|
232 shorturl_id = int(shorturl) |
|
233 url_item = url_item.filter(shorturl__id=shorturl_id) |
|
234 else: |
|
235 url_item = url_item.filter(shorturl__shorturl=shorturl) |
|
236 |
|
237 # match for published items |
|
238 now = timezone.now() |
|
239 url_item = url_item.filter(published__lt=now).order_by('-published') |
|
240 |
|
241 if item_id : |
|
242 # specific, but still the published on |
|
243 log.debug("search @ %d", item_id) |
|
244 |
|
245 return url_item.get(id=item_id) # raises DoesNotExist |
|
246 else : |
|
247 # most recent |
|
248 log.debug("search @ %s", now) |
|
249 try: |
|
250 return url_item[0] |
|
251 except IndexError: |
|
252 raise cls.DoesNotExist() |
|
253 |
|
254 @classmethod |
|
255 def cache_key (cls, shorturl, item_id) : |
|
256 return 'qrurls/url/{shorturl}/{item}'.format(shorturl=shorturl, item=item_id) |
|
257 |
257 @classmethod |
258 @classmethod |
258 def get_item (cls, shorturl, item_id) : |
259 def get_item (cls, shorturl, item_id) : |
259 """ |
260 """ |
260 Return a data dict for the given URLItem, from cache or DB. |
261 Return a data dict for the given URLItem, from cache or DB. |
261 |
262 |
262 Returns { url: str, title: str, image: str, last_modified: datetime } |
263 Returns { url: str, title: str, image: str, last_modified: datetime } |
263 Raises URLItem.NotFound |
264 Raises URLItem.DoesNotExist |
264 """ |
265 """ |
265 |
266 |
266 key = cls.cache_key(shorturl, item_id) |
267 key = cls.cache_key(shorturl, item_id) |
267 get = cache.get(key) |
268 get = cache.get(key) |
268 |
269 |