django-restit 4.2.130__py3-none-any.whl → 4.2.132__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-restit
3
- Version: 4.2.130
3
+ Version: 4.2.132
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -173,7 +173,7 @@ medialib/fixtures/medialib_test_fixture.json,sha256=7M7zvGI2S5G3ENV8OQ3Ks4149lEi
173
173
  medialib/forms.py,sha256=nrE6QTPNPiIeX7Nx4l9DEmAQeQXqFyCg1C3JEDBYJfE,5442
174
174
  medialib/migrations/0001_initial.py,sha256=H3JliH5aw7tiHef8MhrJr_9rGetqgA7UjTF-eKziRSM,20518
175
175
  medialib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
- medialib/models.py,sha256=CrTS5znW8JMuj8gaAnJ5boYvN-deF8zyOuCmW3cFQVE,52228
176
+ medialib/models.py,sha256=snD9Lyskp6i1YTOcERrclPUPIbINkGOhGOTbDKC8dFk,54791
177
177
  medialib/ocr.py,sha256=zlP7-NBiXhW7jR9pljmEPl5xzLVZpLN5QLAELQgU0Fk,1189
178
178
  medialib/pdf.py,sha256=l28WwM0JKbT9boV-b_9TFh9jhvGcrquR8GqC8wfEaLk,1275
179
179
  medialib/qrcode.py,sha256=vHyA5egXOX70EFiUDgr1njI9zcF6bXQJ_hKAQrppRow,545
@@ -205,11 +205,11 @@ medialib/render/presets/video_still.py,sha256=VSWtr6k3UJta3YdOpEfU9d7p46XKi98aPt
205
205
  medialib/render/presets/video_validate.py,sha256=pC6MzzgVWZKGJj6NkQ8SHcDj8z45kxHMuwxK82f8vIU,3228
206
206
  medialib/render/presets/websnap.py,sha256=RSxJ7T2ICU_KQn48yHWLj4qXCABTC7uVIPwe-z_CSkI,863
207
207
  medialib/render/presets/youtube.py,sha256=M4bTZju1u7rrvwLOG89cNUaelHUtnRFss7hggvZCAuE,2282
208
- medialib/render/render_utils.py,sha256=ld0hMfiAWSqC8Nhi408ZPSjKHWYuQXtPdjbeBfav41o,16451
208
+ medialib/render/render_utils.py,sha256=kJlluBmkXoABQqZ6cJFQ3u07cl5ela-Gvu6w96hd6uA,16460
209
209
  medialib/render/schedule.py,sha256=iDSekfsY31SEXF-MR-YdJ9-a3UJ3SoJsZJpiEcbiNZA,370
210
210
  medialib/rpc/__init__.py,sha256=K84yMB6sUr7zTeECxca-_2jyHiUGCHTM-XYmJK6jQCo,82
211
211
  medialib/rpc/legacy.py,sha256=KTNmNJIxfFkZfAj0LXx9-e8bJonOfCoNE0-c_cugsNc,37925
212
- medialib/rpc/media.py,sha256=nZf5OvTZ6vMoJATqOiGJjWBecXkxoXQK9VU521xnVT0,617
212
+ medialib/rpc/media.py,sha256=118Ra_MnCtPZ-oSouA8x3inMb590JH9mwdF--cCqKdM,876
213
213
  medialib/rpc/tools.py,sha256=PQYP0r0NMLU_QdrnKxrMIPNbefzguwe-U8QBm_jhung,956
214
214
  medialib/scripts/init_config,sha256=jfXtOwYcSRFedKeSC8qcZo-ZpyvN8OvYZnQP0uXdURo,7875
215
215
  medialib/static/css/base_medialibui.css,sha256=yxhNcfzSfrx7vkHjBf_KWq-txLdbnVxjCG_dtb7pTqI,4499
@@ -273,7 +273,7 @@ medialib/stores/oauth2client/tools.py,sha256=OLSW5Iu7rtjGankFpjXlHBw8aAFOcIsWdDh
273
273
  medialib/stores/oauth2client/util.py,sha256=1Uc6qwqNhI3b507VtFnklX4sEZNt0MZbLAME2o6kCgg,5706
274
274
  medialib/stores/oauth2client/xsrfutil.py,sha256=4Plq0y5xEkDwvrveVA32gBRP0A3FkJ_36dIsyGHeJeA,3367
275
275
  medialib/stores/rtmpstore.py,sha256=CFRP6Ss4hbLKngbdaLvA8_oKCSe18B29-oWVgvTXMPA,500
276
- medialib/stores/s3.py,sha256=gAFiVHQkbYHZQ_2LU5nl2EXyoSs1mW4VdVCjsImisuE,5836
276
+ medialib/stores/s3.py,sha256=behvLC-ftoMVddJmgK5jfZsC5PVjRBPrSgXkwngNP9A,6280
277
277
  medialib/stores/s3store.py,sha256=uKO6I-X83sD9xefCQytLyrV-7t9TSX2p476n_ZBkSHw,4583
278
278
  medialib/stores/uritemplate/__init__.py,sha256=ONWR_KRz9au0O-XUUTrO_UN7GHTmZCTKyvflUQb8wxM,4996
279
279
  medialib/stores/youtubestore.py,sha256=swXOJYfbhgzAv1NjOehjKtfYRZYk2wiPK7a7_boAulc,406
@@ -299,7 +299,7 @@ medialib/templates/medialib/testpicker.html,sha256=I7KnrAu9e4kQhuEEN-51HGzcuJ06E
299
299
  medialib/tests.py,sha256=z1THDMo-R9HWMvCCOgX6aH6-2W2uWqrtbJ_k-zfpgyc,6045
300
300
  medialib/tq.py,sha256=ZDRBZ9qEsqNAsk48cciLyX7DIziqD2uPEawtQny1Ja8,544
301
301
  medialib/urls.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
302
- medialib/utils.py,sha256=Hw2lZ5vZg_JFjgbt2nhqTYdXwbERCvgnefl8rEdCUKQ,3756
302
+ medialib/utils.py,sha256=StQY7k9dOCksdTsYqUb3aGpwwrcYg07z7vskBQqCPnw,3774
303
303
  medialib/views.py,sha256=h_Fm3FOX04VV2G-J141wEHxT36d46HVdzPJg_u8r2vI,4321
304
304
  medialib/youtube/__init__.py,sha256=fmu9XPaDpadkb2OuroQsKiWpUPo7c5y8o83HmLLMIHA,4303
305
305
  medialib/youtube/apiclient/__init__.py,sha256=iGdQFKwzm0VxArl8X183-_ZBhIsETZlbXxhBAUz3MfI,601
@@ -376,14 +376,14 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
376
376
  rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
377
377
  rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
378
378
  rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
379
- rest/__init__.py,sha256=SVzE--w6i3jhZDS773pK0TONhXDz7k3o4fmQrPAa99w,122
379
+ rest/__init__.py,sha256=-RkqJ1C1aVmCpKUVK79846yJzPNqTf7-orNjsU35Rjo,122
380
380
  rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
381
381
  rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
382
382
  rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
383
383
  rest/crypto/aes.py,sha256=NOVRBRSHCV-om68YpGySWWG-4kako3iEVjq8hxZWPUU,4372
384
384
  rest/crypto/privpub.py,sha256=_FioylVcbMmDP80yPYjURmafEiDmEAMkskbc7WF10ac,4082
385
385
  rest/crypto/util.py,sha256=agFN2OCPHC70tHNGWrMkkZX4Tt_Ty6imoKEMdTkZpKA,4514
386
- rest/datem.py,sha256=JHMvWG8A-n4g915wrZiCtfuhgcLMgNYMXuzXIEtgaPg,12335
386
+ rest/datem.py,sha256=hX6bTbl5mQSg0x2hDK5P1TynkZFUfVTDwYkTuFObgbw,12626
387
387
  rest/decorators.py,sha256=AuB4agpog587CUsF8HkAZiHDfs_pueb2rdxXZD7dUUE,15327
388
388
  rest/encryption.py,sha256=x6Kiez0tVqfxK26MSsRL3k8OS05ni1gEX2aj3I0S9V0,788
389
389
  rest/errors.py,sha256=uKwG9OkLme36etabqK54DMjMQc1fgEoUIAUxXa7WFQw,612
@@ -492,7 +492,7 @@ wiki/renderers/__init__.py,sha256=lLEoJvjU3ezXwBGcjleKk_kMyNeMD9MpfBlEiKayEiM,46
492
492
  wiki/renderers/mistune/__init__.py,sha256=baClLWELOwy5n8UUFi7qmoFBU6QaeegD-wRNZ7fIW_w,84
493
493
  wiki/renderers/mistune/highlight.py,sha256=BosglMQUxc_KVbLapQ4gCt6rCc0rAF1vCtUR7R1Ad4c,1264
494
494
  wiki/renderers/mistune/math.py,sha256=dgQpH9CIDiqyESphoK5XUVFxK5Yb5VhEIoLgjp3Vtcs,1901
495
- wiki/renderers/mistune/media.py,sha256=xD3NJSTYjyZr1mUTeL1QLzB_E5BFprV_12KqPNPYhq8,2610
495
+ wiki/renderers/mistune/media.py,sha256=SYwjhX6_DKfqJ00yLxfKrIHu6JnnwKnroqRm6ySDMfY,2688
496
496
  wiki/renderers/mistune/meta.py,sha256=1lry9m-4wiwsivWnqYHYjwmGv91BGUSB7niJuZ1Xx54,805
497
497
  wiki/renderers/mistune/task_list.py,sha256=Ex0gUPX_d9jtbPbnEEktlqAJsM6wIt5Md2wsltX7LIY,1889
498
498
  wiki/renderers/mistune/toc.py,sha256=TKGiuMVpKqzDGUx5bAjJYpZIzG6n3wTjtuBdBc-TM_8,2302
@@ -512,7 +512,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
512
512
  ws4redis/settings.py,sha256=K0yBiLUuY81iDM4Yr-k8hbvjn5VVHu5zQhmMK8Dtz0s,1536
513
513
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
514
514
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
515
- django_restit-4.2.130.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
516
- django_restit-4.2.130.dist-info/METADATA,sha256=9xgoIJG_Or13Bbw9zbbFb5sByRnWrAdkF2pKjC_0jTU,7663
517
- django_restit-4.2.130.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
518
- django_restit-4.2.130.dist-info/RECORD,,
515
+ django_restit-4.2.132.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
516
+ django_restit-4.2.132.dist-info/METADATA,sha256=NeQ9W-DZO4uEAlKI7xrNkeWyD8YQ60YuSsK3cxTsxjg,7663
517
+ django_restit-4.2.132.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
518
+ django_restit-4.2.132.dist-info/RECORD,,
medialib/models.py CHANGED
@@ -20,6 +20,7 @@ import hashlib
20
20
  import time
21
21
  import mimetypes
22
22
  import tempfile
23
+ import re
23
24
 
24
25
  from medialib import utils
25
26
  from rest import settings
@@ -29,6 +30,7 @@ from rest.decorators import rest_async
29
30
  from taskqueue.models import Task
30
31
 
31
32
 
33
+ MEDIA_FRIENDLY_FILENAMES = settings.get("MEDIA_FRIENDLY_FILENAMES", True)
32
34
  TASKQUEUE_RENDERING = settings.get("TASKQUEUE_RENDERING", False)
33
35
 
34
36
  from rest.log import getLogger
@@ -330,6 +332,7 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
330
332
  Media Item (a video or image)
331
333
  """
332
334
  class RestMeta:
335
+ POST_SAVE_FIELDS = ["rendernow"]
333
336
  SEARCH_FIELDS = ["name", "description"]
334
337
  VIEW_PERMS = ["view_media", "manage_users", "manage_media"]
335
338
  SAVE_PERMS = ["manage_media", "manage_users"]
@@ -451,6 +454,12 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
451
454
  return self.library.default_store()
452
455
  return settings.MEDIALIB_DEFAULT_STORE
453
456
 
457
+ def s3_store(self):
458
+ return f's3://{settings.AWS_S3_BUCKET}'
459
+
460
+ def set_rendernow(self, value):
461
+ self.new_render()
462
+
454
463
  def uses(self):
455
464
  """
456
465
  List uses available for this item
@@ -551,6 +560,11 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
551
560
  return super(MediaItem, self).save()
552
561
 
553
562
  def save(self, *args, **kwargs):
563
+ if self.pk is None:
564
+ return self.saveNew(*args, **kwargs)
565
+ return super(MediaItem, self).save(*args, **kwargs)
566
+
567
+ def saveNew(self, *args, **kwargs):
554
568
  """
555
569
  When saving MediaItem set newfile attribute to upload new file and start rendering
556
570
  """
@@ -1023,6 +1037,25 @@ class MediaItem(models.Model, RestModel, MetaDataModel):
1023
1037
  media.save()
1024
1038
  return media
1025
1039
 
1040
+ @staticmethod
1041
+ def on_upload_s3(request):
1042
+ filename = request.DATA.get("filename")
1043
+ filesize = request.DATA.get("filesize")
1044
+ filetype = request.DATA.get("filetype")
1045
+ kind = utils.guessMediaKindByName(filename)
1046
+ if kind is None:
1047
+ kind = "*"
1048
+ obj = MediaItem(name=filename, owner=request.member, kind=kind, group=request.group)
1049
+ obj.save()
1050
+ rendition = MediaItemRendition(
1051
+ mediaitem=obj, name="Original", bytes=filesize,
1052
+ use='original', kind=kind, is_original=True)
1053
+ rendition.save()
1054
+ rendition.url = rendition.generateURL(filename, store=obj.s3_store())
1055
+ rendition.save()
1056
+ return dict(url=rendition.generateUploadURL(filetype), id=obj.pk)
1057
+
1058
+
1026
1059
  class MediaItemMetaData(MetaDataBase):
1027
1060
  parent = models.ForeignKey(MediaItem, related_name="properties", on_delete=models.CASCADE)
1028
1061
 
@@ -1073,6 +1106,7 @@ class CuePointMeta(models.Model):
1073
1106
  def __str__(self):
1074
1107
  return self.key
1075
1108
 
1109
+
1076
1110
  class MediaItemRendition(models.Model):
1077
1111
  """
1078
1112
  A rendition of a media item
@@ -1119,34 +1153,70 @@ class MediaItemRendition(models.Model):
1119
1153
  except IndexError:
1120
1154
  return ''
1121
1155
 
1122
- def upload(self, fp, prefix=""):
1123
- """
1124
- Upload rendition to non-volatile storage
1125
- """
1126
- doclose = False
1127
- if type(fp) == str:
1128
- doclose = True
1129
- fp = open(fp)
1156
+ def generateUploadURL(self, filetype):
1157
+ from medialib.stores import s3
1158
+ return s3.generate_upload_url(self.url, filetype)
1159
+
1160
+ def generateFriendlyURL(self, name, prefix="", store=None):
1161
+ from rest import helpers as rh
1162
+ rh.log_error("generateFriendlyURL", name, self.use, self.mediaitem.name, " ")
1163
+ if store is None:
1164
+ store = self.mediaitem.default_store()
1165
+ if self.use != "original":
1166
+ if "." in self.mediaitem.name:
1167
+ orig_name = self.mediaitem.name.split('.')
1168
+ orig_ext = orig_name.pop()
1169
+ orig_name = "".join(orig_name)
1170
+ if "." in name:
1171
+ ext = name.split('.')[-1]
1172
+ else:
1173
+ ext = orig_ext
1174
+ name = f"{orig_name}__{self.name}.{ext}"
1175
+ rh.log_error("--- now ---", name, " ")
1176
+ name = name.replace(" ", "_")
1177
+ name = re.sub(r'[^A-Za-z0-9._-]', '', name)
1178
+ paths = []
1179
+ paths.append(store)
1180
+ paths.append("/")
1181
+ paths.append(int_to_base36(self.mediaitem.pk))
1182
+ paths.append("/")
1183
+ paths.append(name)
1184
+ return "".join(paths)
1185
+
1186
+ def generateURL(self, name, prefix="", store=None):
1187
+ if MEDIA_FRIENDLY_FILENAMES:
1188
+ return self.generateFriendlyURL(name, prefix, store)
1130
1189
 
1131
1190
  paths = []
1132
- paths.append(self.mediaitem.default_store())
1191
+ paths.append(store)
1133
1192
  paths.append("/{}/".format(int_to_base36(self.mediaitem.pk)))
1134
1193
  path = "".join(paths)
1135
- if self.rendition_definition != None:
1194
+ if self.rendition_definition is not None:
1136
1195
  paths.append(int_to_base36(self.rendition_definition.pk))
1137
1196
  else:
1138
1197
  paths.append("0")
1139
1198
  paths.append("_")
1140
1199
  paths.append(utils.toMD5(path, int(time.time())))
1141
1200
  paths.append(".")
1142
- ext = ".dat"
1143
- if "." in fp.name:
1144
- ext = fp.name.split('.')[-1]
1201
+ ext = "dat"
1202
+ if "." in name:
1203
+ ext = name.split('.')[-1]
1145
1204
  elif hasattr(self.mediaitem, "ext"):
1146
1205
  ext = self.mediaitem.ext
1147
1206
  paths.append(ext)
1148
1207
  # print(paths)
1149
- self.url = "".join(paths)
1208
+ return "".join(paths)
1209
+
1210
+ def upload(self, fp, prefix=""):
1211
+ """
1212
+ Upload rendition to non-volatile storage
1213
+ """
1214
+ doclose = False
1215
+ if type(fp) == str:
1216
+ doclose = True
1217
+ fp = open(fp)
1218
+
1219
+ self.url = self.generateURL(fp.name, prefix)
1150
1220
  stores.upload(self.url, fp, background=True)
1151
1221
  try:
1152
1222
  self.bytes = fp.size
@@ -343,7 +343,7 @@ def new_rendition(item, renditiondef, fp, kind, width=None, height=None, bytes=N
343
343
  if fp:
344
344
  rendition.upload(fp)
345
345
  elif direct:
346
- rendition.url = fname
346
+ rendition.url = fname + ".bob"
347
347
  rendition.save()
348
348
 
349
349
  if fp:
medialib/rpc/media.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from rest import decorators as rd
2
+ from rest import views as rv
2
3
  from medialib import models as medialib
3
4
 
4
5
 
@@ -9,6 +10,13 @@ def rest_on_media_item(request, pk=None):
9
10
  return medialib.MediaItem.on_rest_request(request, pk)
10
11
 
11
12
 
13
+ @rd.urlPOST('media/item/s3')
14
+ @rd.login_required
15
+ @rd.requires_params(["filename", "filesize"])
16
+ def rest_on_media_upload_s3(request, pk=None):
17
+ return rv.restReturn(request, dict(data=medialib.MediaItem.on_upload_s3(request)))
18
+
19
+
12
20
  @rd.url(r'media/ref/$')
13
21
  @rd.url(r'media/ref/(?P<pk>\d+)$')
14
22
  @rd.login_required
medialib/stores/s3.py CHANGED
@@ -170,6 +170,20 @@ def get_file(url, fp=None):
170
170
  return obj.download(fp)
171
171
 
172
172
 
173
+ def generate_upload_url(url, filetype, expires=3600, acl="public-read"):
174
+ u = urlparse(url)
175
+ bucket_name = u.netloc
176
+ key = u.path.lstrip('/')
177
+ client = getS3(False)
178
+ params = dict(Bucket=bucket_name, Key=key, ContentType=filetype)
179
+ from rest import helpers
180
+ helpers.log_error("generate_upload_url", params)
181
+ return client.generate_presigned_url(
182
+ 'put_object',
183
+ ExpiresIn=expires,
184
+ Params=params)
185
+
186
+
173
187
  def delete(url):
174
188
  if url[-1] == "/":
175
189
  prefix = url.path.lstrip("/")
medialib/utils.py CHANGED
@@ -14,7 +14,8 @@ EXT_MAP = {
14
14
  KIND_MAP = {
15
15
  "video": "V",
16
16
  "image": "I",
17
- "http": "E"
17
+ "http": "E",
18
+ "other": "O"
18
19
  }
19
20
 
20
21
  def getFileExt(file):
rest/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .uberdict import UberDict # noqa: F401
2
2
  from .settings_helper import settings # noqa: F401
3
3
 
4
- __version__ = "4.2.130"
4
+ __version__ = "4.2.132"
rest/datem.py CHANGED
@@ -182,7 +182,9 @@ def getWeek(start, start_day=0):
182
182
  return week_start, week_end
183
183
 
184
184
 
185
- def getStartOfMonth(d):
185
+ def getStartOfMonth(d, clear_time=False):
186
+ if clear_time:
187
+ return d.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
186
188
  return d.replace(day=1)
187
189
 
188
190
 
@@ -190,13 +192,19 @@ def nextMonth(d):
190
192
  return getStartOfMonth(getStartOfMonth(d) + timedelta(days=32))
191
193
 
192
194
 
193
- def getEndOfMonth(start):
195
+ def getEndOfMonthNoTime(start):
194
196
  start = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
195
197
  end = start + timedelta(days=calendar.monthrange(start.year, start.month)[1])
196
198
  end = end.replace(hour=0, minute=0, second=0, microsecond=0)
197
199
  return end
198
200
 
199
201
 
202
+ def getEndOfMonth(d, clear_time=False):
203
+ if clear_time:
204
+ return getEndOfMonthNoTime(d)
205
+ return getStartOfMonth(nextMonth(d)) - timedelta(days=1)
206
+
207
+
200
208
  def parseDate(date_str, is_future=False, is_past=False, month_end=True, as_date=False):
201
209
  res = parseDateTime(date_str, is_future, is_past, month_end)
202
210
  if as_date and res:
@@ -284,10 +292,10 @@ def getDateRange(start, end=None, kind=None, zone=None, hour=0, eod=None, end_eo
284
292
  start, end = getWeek(start)
285
293
  elif kind == "month":
286
294
  start = start.replace(hour=0, day=1)
287
- end = getEndOfMonth(start)
295
+ end = getEndOfMonth(start, True)
288
296
  elif kind == "year":
289
297
  start = start.replace(hour=0, day=1, month=1)
290
- end = getEndOfMonth(start.replace(month=12))
298
+ end = getEndOfMonth(start.replace(month=12), True)
291
299
  elif isinstance(kind, int) or (isinstance(kind, str) and kind.isdigit()):
292
300
  end = start + timedelta(days=1)
293
301
  start = end - timedelta(days=int(kind))
@@ -38,6 +38,7 @@ class MediaMixin(object):
38
38
  return url
39
39
 
40
40
  def link(self, link, text=None, title=None):
41
+ label = text
41
42
  if text is None:
42
43
  text = link
43
44
  o = urllib.parse.urlparse(link)
@@ -47,6 +48,8 @@ class MediaMixin(object):
47
48
  params = {"href":href}
48
49
  if title:
49
50
  params["title"] = title
51
+ if label:
52
+ params["download"] = label
50
53
  if not href.startswith("http"):
51
54
  params["data-action"] = "local_page"
52
55
  flat_params = ' '.join("{}='{}'".format(key,val) for (key,val) in list(params.items()))