summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/accounts/__init__.py1
-rw-r--r--app/accounts/admin.py13
-rw-r--r--app/accounts/apps.py8
-rw-r--r--app/accounts/forms.py28
-rw-r--r--app/accounts/models.py33
-rw-r--r--app/accounts/signals.py12
-rw-r--r--app/accounts/tests/__init__.py0
-rw-r--r--app/accounts/tests/test_models.py13
-rw-r--r--app/accounts/tests/test_views.py23
-rw-r--r--app/accounts/urls.py18
-rw-r--r--app/accounts/views.py32
-rw-r--r--app/builder/__init__.py0
-rw-r--r--app/builder/base.py482
-rw-r--r--app/builder/sanitizer.py60
-rw-r--r--app/builder/views.py37
-rw-r--r--app/classes/__init__.py0
-rw-r--r--app/classes/admin.py45
-rw-r--r--app/classes/migrations/0001_initial.py57
-rw-r--r--app/classes/migrations/0002_alter_class_requires.py18
-rw-r--r--app/classes/migrations/0003_auto_20220109_2043.py25
-rw-r--r--app/classes/migrations/0004_rename_name_class_title.py18
-rw-r--r--app/classes/migrations/0005_auto_20220109_2100.py23
-rw-r--r--app/classes/migrations/0006_auto_20220109_2104.py28
-rw-r--r--app/classes/migrations/0007_session_status.py18
-rw-r--r--app/classes/migrations/0008_class_subtitle.py19
-rw-r--r--app/classes/migrations/__init__.py0
-rw-r--r--app/classes/models.py96
-rw-r--r--app/classes/templates/classes/class_detail.html11
-rw-r--r--app/classes/templates/classes/class_list.html43
-rw-r--r--app/classes/urls.py24
-rw-r--r--app/classes/views.py20
-rw-r--r--app/lib/contact/__init__.py104
-rw-r--r--app/lib/contact/forms.py145
-rw-r--r--app/lib/contact/templates/contact/contact_form.html28
-rw-r--r--app/lib/contact/templates/contact/contact_form.txt0
-rw-r--r--app/lib/contact/templates/contact/contact_form_sent.html14
-rw-r--r--app/lib/contact/templates/contact/contact_form_subject.txt1
-rw-r--r--app/lib/contact/urls.py22
-rw-r--r--app/lib/contact/views.py45
-rw-r--r--app/lib/django_comments/__init__.py104
-rw-r--r--app/lib/django_comments/abstracts.py183
-rw-r--r--app/lib/django_comments/admin.py95
-rw-r--r--app/lib/django_comments/compat.py3
-rw-r--r--app/lib/django_comments/feeds.py33
-rw-r--r--app/lib/django_comments/forms.py200
-rw-r--r--app/lib/django_comments/locale/ar/LC_MESSAGES/django.mobin0 -> 6621 bytes
-rw-r--r--app/lib/django_comments/locale/ar/LC_MESSAGES/django.po322
-rw-r--r--app/lib/django_comments/locale/az/LC_MESSAGES/django.mobin0 -> 4455 bytes
-rw-r--r--app/lib/django_comments/locale/az/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/be/LC_MESSAGES/django.mobin0 -> 6694 bytes
-rw-r--r--app/lib/django_comments/locale/be/LC_MESSAGES/django.po302
-rw-r--r--app/lib/django_comments/locale/bg/LC_MESSAGES/django.mobin0 -> 6396 bytes
-rw-r--r--app/lib/django_comments/locale/bg/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/bn/LC_MESSAGES/django.mobin0 -> 6696 bytes
-rw-r--r--app/lib/django_comments/locale/bn/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/br/LC_MESSAGES/django.mobin0 -> 2131 bytes
-rw-r--r--app/lib/django_comments/locale/br/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/bs/LC_MESSAGES/django.mobin0 -> 5101 bytes
-rw-r--r--app/lib/django_comments/locale/bs/LC_MESSAGES/django.po298
-rw-r--r--app/lib/django_comments/locale/ca/LC_MESSAGES/django.mobin0 -> 5142 bytes
-rw-r--r--app/lib/django_comments/locale/ca/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/cs/LC_MESSAGES/django.mobin0 -> 5490 bytes
-rw-r--r--app/lib/django_comments/locale/cs/LC_MESSAGES/django.po303
-rw-r--r--app/lib/django_comments/locale/cy/LC_MESSAGES/django.mobin0 -> 1124 bytes
-rw-r--r--app/lib/django_comments/locale/cy/LC_MESSAGES/django.po303
-rw-r--r--app/lib/django_comments/locale/da/LC_MESSAGES/django.mobin0 -> 4835 bytes
-rw-r--r--app/lib/django_comments/locale/da/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/de/LC_MESSAGES/django.mobin0 -> 5239 bytes
-rw-r--r--app/lib/django_comments/locale/de/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/el/LC_MESSAGES/django.mobin0 -> 7013 bytes
-rw-r--r--app/lib/django_comments/locale/el/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/en/LC_MESSAGES/django.mobin0 -> 4866 bytes
-rw-r--r--app/lib/django_comments/locale/en/LC_MESSAGES/django.po321
-rw-r--r--app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.mobin0 -> 4576 bytes
-rw-r--r--app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/eo/LC_MESSAGES/django.mobin0 -> 5077 bytes
-rw-r--r--app/lib/django_comments/locale/eo/LC_MESSAGES/django.po296
-rw-r--r--app/lib/django_comments/locale/es/LC_MESSAGES/django.mobin0 -> 5471 bytes
-rw-r--r--app/lib/django_comments/locale/es/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.mobin0 -> 5397 bytes
-rw-r--r--app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.mobin0 -> 5050 bytes
-rw-r--r--app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/et/LC_MESSAGES/django.mobin0 -> 5217 bytes
-rw-r--r--app/lib/django_comments/locale/et/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/eu/LC_MESSAGES/django.mobin0 -> 4725 bytes
-rw-r--r--app/lib/django_comments/locale/eu/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/fa/LC_MESSAGES/django.mobin0 -> 5863 bytes
-rw-r--r--app/lib/django_comments/locale/fa/LC_MESSAGES/django.po294
-rw-r--r--app/lib/django_comments/locale/fi/LC_MESSAGES/django.mobin0 -> 4983 bytes
-rw-r--r--app/lib/django_comments/locale/fi/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/fr/LC_MESSAGES/django.mobin0 -> 5596 bytes
-rw-r--r--app/lib/django_comments/locale/fr/LC_MESSAGES/django.po298
-rw-r--r--app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.mobin0 -> 525 bytes
-rw-r--r--app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.po290
-rw-r--r--app/lib/django_comments/locale/ga/LC_MESSAGES/django.mobin0 -> 5735 bytes
-rw-r--r--app/lib/django_comments/locale/ga/LC_MESSAGES/django.po310
-rw-r--r--app/lib/django_comments/locale/gl/LC_MESSAGES/django.mobin0 -> 4947 bytes
-rw-r--r--app/lib/django_comments/locale/gl/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/he/LC_MESSAGES/django.mobin0 -> 5449 bytes
-rw-r--r--app/lib/django_comments/locale/he/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/hi/LC_MESSAGES/django.mobin0 -> 7513 bytes
-rw-r--r--app/lib/django_comments/locale/hi/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/hr/LC_MESSAGES/django.mobin0 -> 4972 bytes
-rw-r--r--app/lib/django_comments/locale/hr/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/hu/LC_MESSAGES/django.mobin0 -> 5251 bytes
-rw-r--r--app/lib/django_comments/locale/hu/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/ia/LC_MESSAGES/django.mobin0 -> 4981 bytes
-rw-r--r--app/lib/django_comments/locale/ia/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/id/LC_MESSAGES/django.mobin0 -> 5068 bytes
-rw-r--r--app/lib/django_comments/locale/id/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/is/LC_MESSAGES/django.mobin0 -> 4769 bytes
-rw-r--r--app/lib/django_comments/locale/is/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/it/LC_MESSAGES/django.mobin0 -> 5354 bytes
-rw-r--r--app/lib/django_comments/locale/it/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/ja/LC_MESSAGES/django.mobin0 -> 5667 bytes
-rw-r--r--app/lib/django_comments/locale/ja/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/ka/LC_MESSAGES/django.mobin0 -> 7386 bytes
-rw-r--r--app/lib/django_comments/locale/ka/LC_MESSAGES/django.po286
-rw-r--r--app/lib/django_comments/locale/kk/LC_MESSAGES/django.mobin0 -> 5901 bytes
-rw-r--r--app/lib/django_comments/locale/kk/LC_MESSAGES/django.po286
-rw-r--r--app/lib/django_comments/locale/km/LC_MESSAGES/django.mobin0 -> 2141 bytes
-rw-r--r--app/lib/django_comments/locale/km/LC_MESSAGES/django.po285
-rw-r--r--app/lib/django_comments/locale/kn/LC_MESSAGES/django.mobin0 -> 2244 bytes
-rw-r--r--app/lib/django_comments/locale/kn/LC_MESSAGES/django.po286
-rw-r--r--app/lib/django_comments/locale/ko/LC_MESSAGES/django.mobin0 -> 5403 bytes
-rw-r--r--app/lib/django_comments/locale/ko/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/lt/LC_MESSAGES/django.mobin0 -> 5533 bytes
-rw-r--r--app/lib/django_comments/locale/lt/LC_MESSAGES/django.po305
-rw-r--r--app/lib/django_comments/locale/lv/LC_MESSAGES/django.mobin0 -> 5203 bytes
-rw-r--r--app/lib/django_comments/locale/lv/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/mk/LC_MESSAGES/django.mobin0 -> 6409 bytes
-rw-r--r--app/lib/django_comments/locale/mk/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/ml/LC_MESSAGES/django.mobin0 -> 8111 bytes
-rw-r--r--app/lib/django_comments/locale/ml/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/mn/LC_MESSAGES/django.mobin0 -> 6747 bytes
-rw-r--r--app/lib/django_comments/locale/mn/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/nb/LC_MESSAGES/django.mobin0 -> 5027 bytes
-rw-r--r--app/lib/django_comments/locale/nb/LC_MESSAGES/django.po294
-rw-r--r--app/lib/django_comments/locale/ne/LC_MESSAGES/django.mobin0 -> 3364 bytes
-rw-r--r--app/lib/django_comments/locale/ne/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/nl/LC_MESSAGES/django.mobin0 -> 5376 bytes
-rw-r--r--app/lib/django_comments/locale/nl/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/nn/LC_MESSAGES/django.mobin0 -> 4855 bytes
-rw-r--r--app/lib/django_comments/locale/nn/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/pa/LC_MESSAGES/django.mobin0 -> 4496 bytes
-rw-r--r--app/lib/django_comments/locale/pa/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/pl/LC_MESSAGES/django.mobin0 -> 5520 bytes
-rw-r--r--app/lib/django_comments/locale/pl/LC_MESSAGES/django.po304
-rw-r--r--app/lib/django_comments/locale/pt/LC_MESSAGES/django.mobin0 -> 5141 bytes
-rw-r--r--app/lib/django_comments/locale/pt/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.mobin0 -> 5379 bytes
-rw-r--r--app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/ro/LC_MESSAGES/django.mobin0 -> 5590 bytes
-rw-r--r--app/lib/django_comments/locale/ro/LC_MESSAGES/django.po300
-rw-r--r--app/lib/django_comments/locale/ru/LC_MESSAGES/django.mobin0 -> 7482 bytes
-rw-r--r--app/lib/django_comments/locale/ru/LC_MESSAGES/django.po311
-rw-r--r--app/lib/django_comments/locale/sk/LC_MESSAGES/django.mobin0 -> 5243 bytes
-rw-r--r--app/lib/django_comments/locale/sk/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/sl/LC_MESSAGES/django.mobin0 -> 5587 bytes
-rw-r--r--app/lib/django_comments/locale/sl/LC_MESSAGES/django.po309
-rw-r--r--app/lib/django_comments/locale/sq/LC_MESSAGES/django.mobin0 -> 5374 bytes
-rw-r--r--app/lib/django_comments/locale/sq/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/sr/LC_MESSAGES/django.mobin0 -> 6369 bytes
-rw-r--r--app/lib/django_comments/locale/sr/LC_MESSAGES/django.po298
-rw-r--r--app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.mobin0 -> 5143 bytes
-rw-r--r--app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.po298
-rw-r--r--app/lib/django_comments/locale/sv/LC_MESSAGES/django.mobin0 -> 5117 bytes
-rw-r--r--app/lib/django_comments/locale/sv/LC_MESSAGES/django.po299
-rw-r--r--app/lib/django_comments/locale/sw/LC_MESSAGES/django.mobin0 -> 4036 bytes
-rw-r--r--app/lib/django_comments/locale/sw/LC_MESSAGES/django.po290
-rw-r--r--app/lib/django_comments/locale/ta/LC_MESSAGES/django.mobin0 -> 2333 bytes
-rw-r--r--app/lib/django_comments/locale/ta/LC_MESSAGES/django.po291
-rw-r--r--app/lib/django_comments/locale/te/LC_MESSAGES/django.mobin0 -> 5271 bytes
-rw-r--r--app/lib/django_comments/locale/te/LC_MESSAGES/django.po293
-rw-r--r--app/lib/django_comments/locale/th/LC_MESSAGES/django.mobin0 -> 7074 bytes
-rw-r--r--app/lib/django_comments/locale/th/LC_MESSAGES/django.po288
-rw-r--r--app/lib/django_comments/locale/tr/LC_MESSAGES/django.mobin0 -> 5464 bytes
-rw-r--r--app/lib/django_comments/locale/tr/LC_MESSAGES/django.po297
-rw-r--r--app/lib/django_comments/locale/tt/LC_MESSAGES/django.mobin0 -> 488 bytes
-rw-r--r--app/lib/django_comments/locale/tt/LC_MESSAGES/django.po284
-rw-r--r--app/lib/django_comments/locale/uk/LC_MESSAGES/django.mobin0 -> 7044 bytes
-rw-r--r--app/lib/django_comments/locale/uk/LC_MESSAGES/django.po306
-rw-r--r--app/lib/django_comments/locale/ur/LC_MESSAGES/django.mobin0 -> 494 bytes
-rw-r--r--app/lib/django_comments/locale/ur/LC_MESSAGES/django.po290
-rw-r--r--app/lib/django_comments/locale/vi/LC_MESSAGES/django.mobin0 -> 4562 bytes
-rw-r--r--app/lib/django_comments/locale/vi/LC_MESSAGES/django.po286
-rw-r--r--app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.mobin0 -> 4789 bytes
-rw-r--r--app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.po292
-rw-r--r--app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.mobin0 -> 4534 bytes
-rw-r--r--app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.po286
-rw-r--r--app/lib/django_comments/managers.py22
-rw-r--r--app/lib/django_comments/migrations/0001_initial.py74
-rw-r--r--app/lib/django_comments/migrations/0002_update_user_email_field_length.py21
-rw-r--r--app/lib/django_comments/migrations/0003_add_submit_date_index.py20
-rw-r--r--app/lib/django_comments/migrations/0004_auto_20211006_2025.py23
-rw-r--r--app/lib/django_comments/migrations/__init__.py0
-rw-r--r--app/lib/django_comments/models.py62
-rw-r--r--app/lib/django_comments/moderation.py369
-rw-r--r--app/lib/django_comments/signals.py21
-rw-r--r--app/lib/django_comments/templates/comments/400-debug.html116
-rw-r--r--app/lib/django_comments/templates/comments/approve.html16
-rw-r--r--app/lib/django_comments/templates/comments/approved.html8
-rw-r--r--app/lib/django_comments/templates/comments/base.html10
-rw-r--r--app/lib/django_comments/templates/comments/delete.html16
-rw-r--r--app/lib/django_comments/templates/comments/deleted.html8
-rw-r--r--app/lib/django_comments/templates/comments/flag.html17
-rw-r--r--app/lib/django_comments/templates/comments/flagged.html8
-rw-r--r--app/lib/django_comments/templates/comments/form.html21
-rw-r--r--app/lib/django_comments/templates/comments/list.html10
-rw-r--r--app/lib/django_comments/templates/comments/posted.html8
-rw-r--r--app/lib/django_comments/templates/comments/preview.html40
-rw-r--r--app/lib/django_comments/templatetags/__init__.py0
-rw-r--r--app/lib/django_comments/templatetags/comments.py354
-rw-r--r--app/lib/django_comments/urls.py21
-rw-r--r--app/lib/django_comments/views/__init__.py0
-rw-r--r--app/lib/django_comments/views/comments.py142
-rw-r--r--app/lib/django_comments/views/moderation.py166
-rw-r--r--app/lib/django_comments/views/utils.py71
-rw-r--r--app/lib/mdx_attr_list/__init__.py1
-rw-r--r--app/lib/mdx_attr_list/mdx_attr_list.py131
-rw-r--r--app/lib/myoauth/__init__.py0
-rw-r--r--app/lib/myoauth/oauth.py656
-rw-r--r--app/lib/myoauth/oauth.py.bak655
-rw-r--r--app/lib/pagination/__init__.py1
-rw-r--r--app/lib/pagination/middleware.py25
-rw-r--r--app/lib/pagination/models.py1
-rw-r--r--app/lib/pagination/templates/pagination/pagination.html9
-rw-r--r--app/lib/pagination/templatetags/__init__.py1
-rw-r--r--app/lib/pagination/templatetags/pagination_tags.py237
-rw-r--r--app/lib/pagination/tests.py52
-rw-r--r--app/lib/templatetags/__init__.py0
-rw-r--r--app/lib/templatetags/templatetags/__init__.py0
-rw-r--r--app/lib/templatetags/templatetags/admin_reorder.py45
-rw-r--r--app/lib/templatetags/templatetags/amp.py41
-rw-r--r--app/lib/templatetags/templatetags/expense_total.py25
-rw-r--r--app/lib/templatetags/templatetags/f_to_c.py7
-rw-r--r--app/lib/templatetags/templatetags/get_next.py25
-rw-r--r--app/lib/templatetags/templatetags/gravatar_local.py27
-rw-r--r--app/lib/templatetags/templatetags/html5_datetime.py16
-rw-r--r--app/lib/templatetags/templatetags/markdown.py10
-rw-r--r--app/lib/templatetags/templatetags/month_number_to_name.py7
-rw-r--r--app/lib/templatetags/templatetags/nofollow.py15
-rw-r--r--app/lib/templatetags/templatetags/number_to_word.py29
-rw-r--r--app/lib/templatetags/templatetags/slugify_under.py15
-rw-r--r--app/lib/templatetags/templatetags/truncateletters.py24
-rw-r--r--app/lib/upload/__init__.py0
-rw-r--r--app/lib/upload/admin.py50
-rw-r--r--app/lib/upload/flickr.py889
-rw-r--r--app/lib/upload/models.py48
-rw-r--r--app/lib/upload/urls.py10
-rw-r--r--app/lib/upload/views.py95
-rw-r--r--app/lttr/__init__.py0
-rw-r--r--app/lttr/admin.py62
-rw-r--r--app/lttr/forms.py100
-rw-r--r--app/lttr/mailer.py83
-rw-r--r--app/lttr/management/commands/send_newsletter.py31
-rw-r--r--app/lttr/migrations/0001_initial.py78
-rw-r--r--app/lttr/migrations/__init__.py0
-rw-r--r--app/lttr/models.py401
-rw-r--r--app/lttr/modelsnl.py719
-rw-r--r--app/lttr/send.py6
-rw-r--r--app/lttr/templates/lttr/confirm_activate.html24
-rw-r--r--app/lttr/templates/lttr/emails/friends_html_email.html249
-rw-r--r--app/lttr/templates/lttr/emails/friends_plain_text_email.txt16
-rw-r--r--app/lttr/templates/lttr/emails/range_html_email.html239
-rw-r--r--app/lttr/templates/lttr/emails/range_plain_text_email.txt19
-rw-r--r--app/lttr/templates/lttr/emails/test-friends_html_email.html250
-rw-r--r--app/lttr/templates/lttr/emails/test-friends_plain_text_email.txt16
-rw-r--r--app/lttr/templates/lttr/emails/test-range_html_email.html239
-rw-r--r--app/lttr/templates/lttr/emails/test-range_plain_text_email.txt19
-rw-r--r--app/lttr/templates/lttr/message/subscribe.html23
-rw-r--r--app/lttr/templates/lttr/message/subscribe.txt9
-rw-r--r--app/lttr/templates/lttr/message/subscribe_subject.txt1
-rw-r--r--app/lttr/templates/lttr/message/unsubscribe.html19
-rw-r--r--app/lttr/templates/lttr/message/unsubscribe.txt9
-rw-r--r--app/lttr/templates/lttr/message/unsubscribe_subject.txt1
-rw-r--r--app/lttr/templates/lttr/newslettermailing_detail.html155
-rw-r--r--app/lttr/templates/lttr/postcard_subscribe.html29
-rw-r--r--app/lttr/templates/lttr/postcard_subscribed.html24
-rw-r--r--app/lttr/templates/lttr/range_detail.html184
-rw-r--r--app/lttr/templates/lttr/range_list.html44
-rw-r--r--app/lttr/templates/lttr/range_subscribe.html23
-rw-r--r--app/lttr/templates/lttr/subscribe.html23
-rw-r--r--app/lttr/templates/lttr/subscribed.html24
-rw-r--r--app/lttr/templates/lttr/unsubscribe.html24
-rw-r--r--app/lttr/urls.py27
-rw-r--r--app/lttr/validators.py19
-rw-r--r--app/lttr/views.py108
-rw-r--r--app/media/__init__.py0
-rw-r--r--app/media/admin.py37
-rw-r--r--app/media/build.py48
-rw-r--r--app/media/migrations/0001_initial.py111
-rw-r--r--app/media/migrations/__init__.py0
-rw-r--r--app/media/models.py359
-rw-r--r--app/media/photos.js71
-rw-r--r--app/media/resize.py53
-rw-r--r--app/media/retriever.py323
-rw-r--r--app/media/retriever.py.bak314
-rw-r--r--app/media/static/image-preview.js42
-rw-r--r--app/media/static/my_styles.css40
-rw-r--r--app/media/templatetags/__init__.py0
-rw-r--r--app/media/templatetags/get_image_by_size.py8
-rw-r--r--app/media/templatetags/get_image_width.py9
-rw-r--r--app/media/templatetags/get_size_by_name.py8
-rw-r--r--app/media/urls.py74
-rw-r--r--app/media/utils.py28
-rw-r--r--app/media/views.py130
-rw-r--r--app/normalize/__init__.py0
-rw-r--r--app/normalize/admin.py13
-rw-r--r--app/normalize/migrations/0001_initial.py31
-rw-r--r--app/normalize/migrations/0002_alter_relatedpost_id.py18
-rw-r--r--app/normalize/migrations/__init__.py0
-rw-r--r--app/normalize/models.py17
-rw-r--r--app/pages/__init__.py0
-rw-r--r--app/pages/admin.py55
-rw-r--r--app/pages/build.py25
-rw-r--r--app/pages/migrations/0001_initial.py49
-rw-r--r--app/pages/migrations/0002_auto_20211031_1354.py30
-rw-r--r--app/pages/migrations/__init__.py0
-rw-r--r--app/pages/models.py61
-rw-r--r--app/pages/static.py123
-rw-r--r--app/pages/templates/pages/about.html32
-rw-r--r--app/pages/templates/pages/homepage.html32
-rw-r--r--app/pages/templates/pages/page_detail.html38
-rw-r--r--app/pages/templates/pages/page_detail.txt8
-rw-r--r--app/pages/urls.py13
-rw-r--r--app/pages/views.py40
-rw-r--r--app/posts/__init__.py0
-rw-r--r--app/posts/admin.py77
-rw-r--r--app/posts/build.py110
-rw-r--r--app/posts/importer.py107
-rw-r--r--app/posts/migrations/0001_initial.py56
-rw-r--r--app/posts/migrations/0002_alter_post_post_type.py18
-rw-r--r--app/posts/migrations/__init__.py0
-rw-r--r--app/posts/models.py275
-rw-r--r--app/posts/templates/horizontal_select.html17
-rw-r--r--app/posts/templates/posts/essay_detail.html178
-rw-r--r--app/posts/templates/posts/essay_list.html24
-rw-r--r--app/posts/templates/posts/fieldnote_archive_list_date.html43
-rw-r--r--app/posts/templates/posts/fieldnote_detail.html114
-rw-r--r--app/posts/templates/posts/fieldnote_list.html54
-rw-r--r--app/posts/templates/posts/guide_base.html41
-rw-r--r--app/posts/templates/posts/guide_detail.html187
-rw-r--r--app/posts/templates/posts/jrnl_date.html42
-rw-r--r--app/posts/templates/posts/jrnl_detail.html240
-rw-r--r--app/posts/templates/posts/jrnl_detail.txt8
-rw-r--r--app/posts/templates/posts/jrnl_latest.html6
-rw-r--r--app/posts/templates/posts/jrnl_list.html33
-rw-r--r--app/posts/templates/posts/post_detail.html183
-rw-r--r--app/posts/templates/posts/post_detail.txt8
-rw-r--r--app/posts/templates/posts/post_list.html40
-rw-r--r--app/posts/templates/posts/src_detail.html110
-rw-r--r--app/posts/templates/posts/src_list.html30
-rw-r--r--app/posts/urls/__init__old.py4
-rw-r--r--app/posts/urls/essay_urls.py29
-rw-r--r--app/posts/urls/field_note_urls.py39
-rw-r--r--app/posts/urls/guide_urls.py30
-rw-r--r--app/posts/urls/guide_urls_old.py29
-rw-r--r--app/posts/urls/jrnl_urls.py60
-rw-r--r--app/posts/urls/review_urls.py29
-rw-r--r--app/posts/urls/src_urls.py49
-rw-r--r--app/posts/views/__init__.py19
-rw-r--r--app/posts/views/field_note_views.py39
-rw-r--r--app/posts/views/guide_views.py96
-rw-r--r--app/posts/views/jrnl_views.py173
-rw-r--r--app/posts/views/src_views.py97
-rw-r--r--app/taxonomy/admin.py54
-rw-r--r--app/taxonomy/migrations/0001_initial.py57
-rw-r--r--app/taxonomy/migrations/0002_auto_20211006_2025.py28
-rw-r--r--app/taxonomy/migrations/__init__.py0
-rw-r--r--app/taxonomy/models.py44
-rw-r--r--app/taxonomy/urls.py13
-rw-r--r--app/taxonomy/views.py14
-rw-r--r--app/utils/__init__.py0
-rw-r--r--app/utils/next_prev.py80
-rw-r--r--app/utils/static/autocomplete.js10
-rw-r--r--app/utils/static/choices.mapfix.css20
-rw-r--r--app/utils/static/choices.min.css5
-rw-r--r--app/utils/static/choices.min.js5
-rw-r--r--app/utils/static/image-loader.js47
-rw-r--r--app/utils/static/next-prev-links.js60
-rw-r--r--app/utils/urls.py12
-rw-r--r--app/utils/util.py147
-rw-r--r--app/utils/views.py105
-rw-r--r--app/utils/widgets.py144
386 files changed, 37600 insertions, 0 deletions
diff --git a/app/accounts/__init__.py b/app/accounts/__init__.py
new file mode 100644
index 0000000..9332741
--- /dev/null
+++ b/app/accounts/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'accounts.apps.AccountAppConfig'
diff --git a/app/accounts/admin.py b/app/accounts/admin.py
new file mode 100644
index 0000000..25f873b
--- /dev/null
+++ b/app/accounts/admin.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+
+from .models import User, UserProfile
+
+
+@admin.register(User)
+class UserAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(UserProfile)
+class UserProfileAdmin(admin.ModelAdmin):
+ pass
diff --git a/app/accounts/apps.py b/app/accounts/apps.py
new file mode 100644
index 0000000..9a7d5ba
--- /dev/null
+++ b/app/accounts/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class AccountAppConfig(AppConfig):
+ name = 'accounts'
+
+ def ready(self):
+ import accounts.signals
diff --git a/app/accounts/forms.py b/app/accounts/forms.py
new file mode 100644
index 0000000..2da08cb
--- /dev/null
+++ b/app/accounts/forms.py
@@ -0,0 +1,28 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from django_registration.forms import RegistrationForm
+
+from .models import User, UserProfile
+
+
+class UserForm(RegistrationForm):
+ class Meta(RegistrationForm.Meta):
+ model = User
+
+
+class ProfileForm(forms.ModelForm):
+ class Meta:
+ model = UserProfile
+ fields = ['bio', 'photo', 'website']
+ labels = {
+ "photo": _("Profile photo"),
+ "bio": _("Bio. A little about you. links are fine, line breaks are not. Keep it short and sweet, 350 characters max"),
+ "website": _("If you have a personal website, plug it in here."),
+ }
+ widgets = {
+ 'bio': forms.Textarea(attrs={'cols': 104, 'rows': 10, 'class': 'textarea-rounded'}),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.user = kwargs.pop("user", None)
+ super(ProfileForm, self).__init__(*args, **kwargs)
diff --git a/app/accounts/models.py b/app/accounts/models.py
new file mode 100644
index 0000000..feb20bf
--- /dev/null
+++ b/app/accounts/models.py
@@ -0,0 +1,33 @@
+from django.db import models
+from django.urls import reverse
+from django.contrib.auth.models import AbstractUser
+from django.utils.functional import cached_property
+
+from notes.models import Notebook
+
+
+class User(AbstractUser):
+ pass
+
+ class Meta:
+ ordering = ['-date_joined']
+
+
+class UserProfile(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
+ photo = models.ImageField(upload_to='profile', null=True, blank=True)
+ website = models.CharField(max_length=300, blank=True, default='')
+ location = models.CharField(max_length=300, blank=True, default='')
+ bio = models.CharField(max_length=350, blank=True, default='')
+ #default_note_folder = models.ForeignKey('notes.Notebook', null=True, on_delete=models.SET_NULL)
+ #default_note_public = models.BooleanField(default=False)
+
+ def __str__(self):
+ return self.user.username
+
+ def get_absolute_url(self):
+ return reverse("accounts:settings")
+
+ @cached_property
+ def get_notebook_list(self):
+ return Notebook.objects.filter(owner=self.user).select_related().annotate(note_count=models.Count('note'))[:8]
diff --git a/app/accounts/signals.py b/app/accounts/signals.py
new file mode 100644
index 0000000..837a7ed
--- /dev/null
+++ b/app/accounts/signals.py
@@ -0,0 +1,12 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from .models import User, UserProfile
+
+
+@receiver(post_save, sender=User)
+def create_profile(sender, update_fields, created, instance, **kwargs):
+ """ creates a blank profile when a new user signs up """
+ if created:
+ user_profile = UserProfile.objects.create(user=instance)
+ user_profile.save()
diff --git a/app/accounts/tests/__init__.py b/app/accounts/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/accounts/tests/__init__.py
diff --git a/app/accounts/tests/test_models.py b/app/accounts/tests/test_models.py
new file mode 100644
index 0000000..f9b33f6
--- /dev/null
+++ b/app/accounts/tests/test_models.py
@@ -0,0 +1,13 @@
+from django.test import TestCase
+from mixer.backend.django import mixer
+
+from accounts.models import User, UserProfile
+
+
+class UserProfileModelTest(TestCase):
+
+ def test_string_representation(self):
+ user = mixer.blend(User, username='test')
+ user.save()
+ profile = UserProfile.objects.get(user=user)
+ self.assertEqual(str(profile), str(user.username))
diff --git a/app/accounts/tests/test_views.py b/app/accounts/tests/test_views.py
new file mode 100644
index 0000000..39dcb31
--- /dev/null
+++ b/app/accounts/tests/test_views.py
@@ -0,0 +1,23 @@
+from django.test import Client
+from django.test import RequestFactory, TestCase
+from mixer.backend.django import mixer
+
+from accounts.models import User
+from accounts.views import ProfileView
+
+
+class ProfileViewTest(TestCase):
+ def setUp(self):
+ # Every test needs access to the request factory.
+ self.factory = RequestFactory()
+ self.user = mixer.blend(User, username='tpynchon', password="gravity")
+
+ def test_profile_view(self):
+ request = self.factory.get('/settings/')
+ request.user = self.user
+ response = ProfileView.as_view()(request)
+ self.assertEqual(response.status_code, 200)
+ response.render()
+ html = response.content.decode('utf8')
+ self.assertTrue(html.startswith('<!DOCTYPE html>'))
+ self.assertIn('<h1>Account Settings</h1>', html)
diff --git a/app/accounts/urls.py b/app/accounts/urls.py
new file mode 100644
index 0000000..5cad311
--- /dev/null
+++ b/app/accounts/urls.py
@@ -0,0 +1,18 @@
+from django.urls import path
+
+from . import views
+
+app_name = "accounts"
+
+urlpatterns = [
+ path(
+ r'change-profile/',
+ views.ProfileView.as_view(),
+ name="change-profile"
+ ),
+ path(
+ r'',
+ views.SettingsListView.as_view(),
+ name="settings"
+ ),
+]
diff --git a/app/accounts/views.py b/app/accounts/views.py
new file mode 100644
index 0000000..75bb933
--- /dev/null
+++ b/app/accounts/views.py
@@ -0,0 +1,32 @@
+from django.views.generic import UpdateView, DetailView
+from django.utils.decorators import method_decorator
+from django.contrib.auth.decorators import login_required
+
+from .models import UserProfile
+from .forms import ProfileForm
+
+
+@method_decorator(login_required, name='dispatch')
+class UpdateViewWithUser(UpdateView):
+
+ def get_form_kwargs(self, **kwargs):
+ kwargs = super().get_form_kwargs(**kwargs)
+ kwargs.update({'user': self.request.user})
+ return kwargs
+
+
+class ProfileView(UpdateViewWithUser):
+ model = UserProfile
+ form_class = ProfileForm
+ template_name = "accounts/change-settings.html"
+
+ def get_object(self):
+ return self.request.user.profile
+
+
+class SettingsListView(DetailView):
+ model = UserProfile
+ template_name = "accounts/profile.html"
+
+ def get_object(self):
+ return self.request.user.profile
diff --git a/app/builder/__init__.py b/app/builder/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/builder/__init__.py
diff --git a/app/builder/base.py b/app/builder/base.py
new file mode 100644
index 0000000..532deea
--- /dev/null
+++ b/app/builder/base.py
@@ -0,0 +1,482 @@
+import os
+from math import ceil
+from decimal import Decimal
+from django.contrib.sites.models import Site
+from django.test.client import Client
+from django.template.loader import render_to_string
+from django.template import Context
+from django.urls import reverse
+from django.apps import apps
+from django.conf import settings
+from jsmin import jsmin
+
+
+class _FileWriter(object):
+ """
+ Given a path and text object; write the page to disc
+ """
+ def __init__(self, path, text_object, ext='html', filename='index', site='luxagraf.net'):
+ site = Site.objects.get(domain=site)
+ base_path = os.path.join(settings.PROJ_ROOT, site.domain)
+ self.path = '%s%s' % (base_path, path)
+ if not os.path.isdir(self.path):
+ os.makedirs(self.path)
+ fpath = '%s%s.%s' % (self.path, filename, ext)
+ self.write(fpath, text_object)
+
+ def write(self, fpath, text_object):
+ f = open(fpath, 'wb')
+ f.write(text_object)
+ f.close()
+
+ def compress_js(self, filename, text_object):
+ path = '%s%s.min.js' % (self.path, filename)
+ compressed = jsmin(text_object.decode('utf-8')).encode('utf-8')
+ self.write(path, compressed)
+
+
+class BuildNew():
+
+ def __init__(self, model, app, site='luxagraf.net'):
+ self.site = Site.objects.get(domain=site)
+ self.model = apps.get_model(model, app)
+ self.get_model_queryset()
+ self.client = Client()
+
+ def build(self):
+ self.build_list_view()
+ self.build_detail_view()
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(status__exact=1)
+
+ def write_file(self, path, text_object, ext='html', filename='index'):
+ self.writer = _FileWriter(path, text_object, ext=ext, filename=filename, site=self.site)
+
+ def get_pages(self, qs, paginate_by):
+ return int(ceil(Decimal(qs.count()) / Decimal(paginate_by)))
+
+ def build_list_view(self, base_path='', qs=None, paginate_by=10):
+ """
+ Archive Page builder that actually crawls the urls
+ because we need to be able to pass a request object to the template
+ """
+
+ if not qs:
+ qs = self.get_model_queryset()
+ pages = self.get_pages(qs, paginate_by)
+ for page in range(pages):
+ if int(pages) > 1:
+ path = '%s%s/' % (base_path, str(page + 1))
+ url = '%s%s/' % (base_path, str(page + 1))
+ else:
+ path = base_path
+ url = base_path
+ print(path)
+ response = self.client.get(url, HTTP_HOST='127.0.0.1', follow=True)
+ if page == 0:
+ self.write_file(base_path, response.content)
+ self.write_file(path, response.content)
+
+ def build_year_view(self, url, paginate_by=99999):
+ years = self.get_model_queryset().dates('pub_date', 'year')
+ for year in years:
+ year = year.strftime('%Y')
+ qs = self.model.objects.filter(
+ status__exact=1,
+ pub_date__year=year
+ )
+ self.build_list_view(
+ base_path=reverse(url, kwargs={'year': year, }),
+ qs=qs,
+ paginate_by=paginate_by
+ )
+
+ def build_month_view(self, url, paginate_by=99999):
+ months = self.get_model_queryset().dates('pub_date', 'month')
+ for m in months:
+ year = m.strftime('%Y')
+ month = m.strftime('%m')
+ qs = self.model.objects.filter(
+ status__exact=1,
+ pub_date__year=year,
+ pub_date__month=month
+ )
+ if qs.exists():
+ self.build_list_view(
+ base_path=reverse(url, kwargs={'year': year, 'month': month}),
+ qs=qs,
+ paginate_by=paginate_by
+ )
+
+ def build_detail_view(self):
+ '''
+ Grab all the blog posts, render them to a template
+ string and write that out to the filesystem
+ '''
+ for entry in self.get_model_queryset():
+ url = entry.get_absolute_url()
+ path, slug = os.path.split(entry.get_absolute_url())
+ path = '%s/' % path
+ # write html
+ response = self.client.get(url)
+ self.write_file(path, response.content, filename=slug)
+ # write txt
+ response = self.client.get('%s.txt' % url)
+ self.write_file(path, response.content, ext='txt', filename=slug)
+
+
+ def build_feed(self, url_name):
+ """
+ Not called, but available for subclassing
+ """
+ url = reverse(url_name,)
+ path, slug = os.path.split(url)
+ slug, ext = os.path.splitext(slug)
+ response = self.client.get(url, HTTP_HOST='127.0.0.1')
+ self.write_file('%s/' % path, response.content, ext=ext.split(".")[-1], filename=slug)
+
+class Build():
+
+ def write_file(self, path, text_object, ext='html', filename='index'):
+ """
+ Given a path and object intended to be a webpage, write the page the
+ disc
+ """
+ path = '%s%s' % (settings.FLATFILES_ROOT, path)
+ if not os.path.isdir(path):
+ os.makedirs(path)
+ fpath = '%s%s.%s' % (path, filename, ext)
+ file = open(fpath, 'wb')
+ file.write(text_object)
+ file.close()
+ if ext == 'js':
+ from jsmin import jsmin
+ fpath = '%s%s.min.%s' % (path, filename, ext)
+ compressed = jsmin(text_object.decode(encoding='UTF-8'))
+ with open(fpath, 'wb') as js_file:
+ minified = js_file.write(compressed.encode('utf-8'))
+ js_file.close()
+
+ def build_archive_pages(self, qs=None, base_path='', paginate_by=10):
+ """
+ Archive Page builder that actually crawls the urls
+ because we need to be able to pass a request object to the template
+
+ """
+ if qs is None:
+ qs = self.get_model_querset()
+ c = Client()
+ pages = ceil(Decimal(qs.count()) / Decimal(paginate_by))
+ for page in range(int(pages)):
+ path = '%s%s/' % (base_path, page + 1)
+ url = '/%s%s/' % (base_path, str(page + 1))
+ page_url = base_path + '%d/'
+ response = c.post(url, {'page_url': page_url, 'page': int(page), 'builder': True}, HTTP_HOST='127.0.0.1')
+ if page == 0:
+ self.write_file(base_path, response.content)
+ self.write_file(path, response.content)
+
+class BuildAll(Build):
+ def build(self):
+ BuildWriting().build()
+ BuildPhotos().build()
+ BuildProjects().build()
+ BuildMap().build()
+ BuildWritingFeed().build()
+ BuildSitemap().build()
+ BuildPages().build()
+ p.write_files()
+
+class BuildWriting(Build):
+ def build(self):
+ self.build_detail_pages()
+ self.build_writing_archives()
+ self.build_country_archive_pages()
+ self.build_region_archive_pages()
+ self.build_homepage()
+ self.build_404()
+ self.writing_year_archives()
+ self.writing_month_archives()
+
+ def get_model_querset(self):
+ model = apps.get_model('jrnl', 'entry')
+ qs = model.objects.filter(status__exact=1)
+ return qs
+
+ def build_detail_pages(self):
+ '''
+ Grab all the blog posts, render them to a template string and write that out to the filesystem
+ '''
+ qs = self.get_model_querset()
+ for entry in qs:
+ c = {
+ 'object': entry,
+ 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL
+ }
+ t = render_to_string('details/entry.html', c).encode('utf-8')
+ path = '/jrnl/%s/' %(entry.pub_date.strftime("%Y/%m").lower())
+ slug = '%s' %(entry.slug)
+ self.write_file(path, t, 'html', slug)
+ s = render_to_string('details/entry.txt',c).encode('utf-8')
+ self.write_file(path, s,'txt', slug)
+
+ def build_writing_archives(self):
+ qs = self.get_model_querset()
+ self.build_archive_pages(qs, 'jrnl/')
+
+ def build_region_archive_pages(self):
+ model = apps.get_model('locations', 'Region')
+ blog = apps.get_model('jrnl', 'entry')
+ regions = model.objects.all()
+ for c in regions:
+ qs = blog.objects.filter(status__exact=1, location__state__country__lux_region=c.id).order_by('-pub_date')
+ path = 'jrnl/%s/' % (c.slug)
+ self.build_archive_pages(qs, path)
+
+ def build_country_archive_pages(self):
+ model = apps.get_model('locations', 'Country')
+ blog = apps.get_model('jrnl', 'entry')
+ countries = model.objects.filter(visited=True)
+ for c in countries:
+ qs = blog.objects.filter(status__exact=1, location__state__country=c).order_by('-pub_date')
+ path = 'jrnl/%s/' % (c.slug)
+ self.build_archive_pages(qs, path)
+
+ def writing_year_archives(self):
+ entry = apps.get_model('jrnl', 'entry')
+ years = entry.objects.dates('pub_date', 'year')
+ for year in years:
+ year = year.strftime('%Y')
+ qs = entry.objects.filter(status__exact=1, pub_date__year=year).order_by('pub_date')
+ c = Context({'type': 'year', 'year': year, 'object_list': qs})
+ t = render_to_string('archives/writing_date.html', c).encode('utf-8')
+ fpath = 'jrnl/%s/' % (year)
+ self.write_file(fpath, t)
+
+ def writing_month_archives(self):
+ entry = apps.get_model('jrnl', 'entry')
+ months = entry.objects.dates('pub_date', 'month')
+ for m in months:
+ year = m.strftime('%Y')
+ month = m.strftime('%m')
+ month_name = m.strftime('%b')
+ month_full_name = m.strftime('%B')
+ qs = entry.objects.filter(status__exact=1, pub_date__year=year,
+ pub_date__month=month).order_by('pub_date')
+ c = Context({'type': 'monthly', 'year': year, 'month': month_full_name, 'object_list': qs, })
+ t = render_to_string('archives/writing_date.html', c).encode('utf-8')
+ fpath = 'jrnl/%s/%s/' % (year, month)
+ self.write_file(fpath, t)
+
+ def build_homepage(self):
+ obj = apps.get_model('jrnl', 'homepagecurrator').objects.get(pk=1)
+ recent = apps.get_model('jrnl', 'entry').objects.filter(status__exact=1)[:4]
+ template = obj.template_name
+ c = Context({'homepage': obj, 'recent': recent, 'MEDIA_URL': settings.BAKED_MEDIA_URL, 'IMAGES_URL': settings.BAKED_IMAGES_URL})
+ t = render_to_string(template, c).encode('utf-8')
+ self.write_file('', t)
+
+ def build_404(self):
+ c = Context()
+ t = render_to_string('404.html', c).encode('utf-8')
+ self.write_file('', t, 'html', '404')
+
+class BuildPhotos(Build):
+ def build(self):
+ self.build_photo_archive_pages()
+ self.build_detail_pages()
+ self.build_js()
+
+ def build_photo_archive_pages(self):
+ qs = apps.get_model('photos', 'PhotoGallery').objects.all()
+ self.build_archive_pages(qs, 'photos/', 18)
+
+ def build_detail_pages(self):
+ qs = apps.get_model('photos', 'PhotoGallery').objects.all()
+ for photo in qs:
+ c = Context({'object': photo, 'MEDIA_URL':
+ settings.BAKED_MEDIA_URL, 'IMAGES_URL': settings.BAKED_IMAGES_URL})
+ t = render_to_string('details/photo_galleries.html', c).encode('utf-8')
+ path = 'photos/galleries/%s/' % (photo.set_slug)
+ self.write_file(path, t)
+
+ def build_js(self):
+ fpath = '%sdesign/templates/js/leaflet-providers.js' % settings.PROJ_ROOT
+ leaflet_providers_js = open(fpath, 'r').read()
+ fpath = '%sapp/photos/photos.js' % settings.PROJ_ROOT
+ photos_js = open(fpath, 'r', encoding='UTF8').read()
+ js = leaflet_providers_js + photos_js
+ self.write_file('media/js/', js.encode('utf-8'), 'js', 'photos')
+
+class BuildProjects(Build):
+ def build(self):
+ self.build_project_archive()
+ self.build_project_details()
+ self.build_project_data()
+ self.build_gifs()
+ self.build_np_basejs()
+
+ def get_projects(self):
+ all_proj = []
+ proj = apps.get_model('projects', 'Project').objects.get(pk=2)
+ row = {'slug': proj.slug, 'name': proj.model_name}
+ all_proj.append(row)
+ return all_proj
+
+ def build_project_archive(self):
+ qs = apps.get_model('projects', 'Project').objects.filter(status__exact=1).order_by('-pub_date')
+ c = {'object_list': qs, 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL}
+ t = render_to_string('archives/projects.html', c).encode('utf-8')
+ self.write_file('projects/', t)
+
+ def build_project_details(self):
+ projects = self.get_projects()
+ for proj in projects:
+ model = apps.get_model('projects', proj['name'])
+ if proj['name'] == 'NationalParks':
+ qs = model.objects.filter(visited__exact=True).order_by("-date_visited_begin")
+ else:
+ qs = model.objects.filter(status__exact=1)
+ c = {
+ 'object_list': qs,
+ 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL
+ }
+ t = render_to_string('details/%s.html' % (proj['slug'].split("/")[1]), c).encode('utf-8')
+ path = 'projects/%s/' % (proj['slug'].split("/")[1])
+ self.write_file(path, t)
+
+ """
+ not sure how to handle projects really, the above doesn't work and
+ if I just keep writing if/else statements that gets messy, so I guess
+ functions it is.
+ """
+ def build_gifs(self):
+ qs = apps.get_model('projects', 'AnimatedGif').objects.all()
+ for gif in qs:
+ c = {
+ 'object': gif,
+ 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL
+ }
+ t = render_to_string('details/gifs.html', c).encode('utf-8')
+ path = 'projects/gifs/%s/' % (gif.slug)
+ self.write_file(path, t)
+
+ def build_project_data(self):
+ model = apps.get_model('projects', 'NationalParks')
+ for park in model.objects.filter(visited__exact=True):
+ path = 'projects/data/natparks/'
+ json = park.mpoly.json
+ self.write_file(path, json.encode('utf-8'), 'json', park.id)
+
+ def build_np_basejs(self):
+ fpath = '%sdesign/templates/js/leaflet-providers.js' % settings.PROJ_ROOT
+ leaflet_providers_js = open(fpath, 'r').read()
+ fpath = '%sapp/projects/natparks.js' % settings.PROJ_ROOT
+ natparks_js = open(fpath, 'r').read()
+ js = leaflet_providers_js + natparks_js
+ self.write_file('media/js/', js.encode('utf-8'), 'js', 'natparks')
+
+class BuildSitemap(Build):
+ def build(self):
+ c = Client()
+ response = c.get('/sitemap.xml', HTTP_HOST='127.0.0.1')
+ self.write_file('', response.content, 'xml', 'sitemap')
+
+
+class BuildWritingFeed(Build):
+ def build(self):
+ qs = apps.get_model('blog', 'entry').objects.filter(status__exact=1).order_by('-pub_date')[:20]
+ c = Context({'object_list': qs, 'SITE_URL': settings.SITE_URL})
+ t = render_to_string('feed.xml', c).encode('utf-8')
+ fpath = '%s' % ('rss/',)
+ self.write_file(fpath, t, 'xml')
+
+class BuildPages(Build):
+ def build(self):
+ model = apps.get_model('pages', 'page')
+ pages = model.objects.all()
+ for page in pages:
+ c = Context({'object':page,'SITE_URL':settings.SITE_URL, 'MEDIA_URL':settings.BAKED_MEDIA_URL})
+ t = render_to_string(["details/%s.html" % page.slug, 'details/page.html'],c).encode('utf-8')
+ s = render_to_string('details/page.txt',c).encode('utf-8')
+ fpath = '%s' %(page.slug)
+ self.write_file('', t, 'html', page.slug)
+ self.write_file('', t, 'txt', page.slug)
+
+class BuildMap(Build):
+ def build(self):
+ qs = apps.get_model('jrnl', 'entry').objects.filter(status__exact=1)
+ cl = apps.get_model('locations', 'Country').objects.filter(visited=True).exclude(name='default')
+ rl = apps.get_model('locations', 'Region').objects.all()
+ rtl = apps.get_model('locations', 'Route').objects.all()
+ c = Context({
+ 'object_list': qs,
+ 'country_list': cl,
+ 'region_list': rl,
+ 'route_list': rtl,
+ 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL
+ })
+ t = render_to_string('archives/map_data.html', c).encode('utf-8')
+ fpath = '%sdesign/templates/js/leaflet-providers.js' % settings.PROJ_ROOT
+ leaflet_providers_js = open(fpath, 'r').read()
+ js = leaflet_providers_js + t.decode(encoding='utf-8')
+ self.write_file('media/js/', js.encode('utf-8'), 'js', 'mainmap')
+ c = Context({
+ 'country_list': cl,
+ 'region_list': rl,
+ 'route_list': rtl,
+ 'MEDIA_URL': settings.BAKED_MEDIA_URL,
+ 'IMAGES_URL': settings.BAKED_IMAGES_URL
+ })
+ t = render_to_string('archives/map.html', c).encode('utf-8')
+ self.write_file('', t, "html",'map')
+
+
+# Back up entries to markdown text files which are then stored in dropbox and git
+class EntryBak(Build):
+ def get_model_querset(self):
+ model = apps.get_model('jrnl', 'entry')
+ qs = model.objects.filter(status__exact=1)
+ return qs
+
+ def write_file(self, path, text_object):
+ file = open(path, 'wb')
+ file.write(text_object)
+ file.close()
+
+ def build_writing_bak(self):
+ qs = self.get_model_querset()
+ for obj in qs:
+ c = Context({'object': obj, 'MEDIA_URL': settings.BAKED_MEDIA_URL, 'IMAGES_URL': settings.BAKED_IMAGES_URL})
+ path = "%szbak/posts/%s_%s.txt" %(settings.PROJ_ROOT, (obj.pub_date.strftime("%Y-%m-%d").lower()), obj.slug)
+ t = render_to_string('details/entry-bak.txt', c).encode('utf-8')
+ self.write_file(path, t)
+
+
+
+class BuildBooks(Build):
+ def build(self):
+ self.build_detail_pages()
+ self.build_book_archive_pages()
+
+
+ def build_book_archive_pages(self):
+ qs = apps.get_model('books', 'Book').objects.all().order_by('-read_date').select_related()
+ print(qs)
+ self.build_archive_pages(qs, 'books/', 18)
+
+
+ def build_detail_pages(self):
+ qs = apps.get_model('books', 'Book').objects.all().order_by('-read_date').select_related()
+ for book in qs:
+ c = Context({'object': book,})
+ t = render_to_string('details/book.html', c).encode('utf-8')
+ path = 'books/'
+ slug = '%s' % (book.slug)
+ self.write_file(path, t, 'html', slug)
diff --git a/app/builder/sanitizer.py b/app/builder/sanitizer.py
new file mode 100644
index 0000000..8512f4f
--- /dev/null
+++ b/app/builder/sanitizer.py
@@ -0,0 +1,60 @@
+from bs4 import BeautifulSoup
+
+
+class Sanitizer(object):
+ blacklisted_tags = []
+ blacklisted_attributes = []
+ blacklisted_protocols = []
+
+ def __init__(self, tags=None, attributes=None, protocols=None):
+ if tags:
+ self.blacklisted_tags = tags
+ if attributes:
+ self.blacklisted_attributes = attributes
+ if protocols:
+ self.blacklisted_protocols = protocols
+
+ def strip(self, content=None):
+ """Strip HTML content to meet standards of output type.
+ Meant to be subclassed for each converter.
+
+ Keyword arguments:
+ content -- subset of an HTML document. (ie. contents of a body tag)
+ """
+ if not content:
+ content = self.content
+ return content
+
+ soup = BeautifulSoup(content, "lxml")
+ self.strip_tags(soup)
+ self.strip_attributes(soup)
+
+ output = soup.body.decode_contents()
+ return output
+
+ def strip_tags(self, soup):
+ if self.blacklisted_tags:
+ [x.extract() for x in soup.find_all(self.blacklisted_tags)]
+
+ def strip_attributes_extra(self, node):
+ pass
+
+ def strip_attributes(self, soup):
+ if not (self.blacklisted_attributes or self.blacklisted_protocols):
+ return
+
+ for node in soup.body.find_all(True):
+ attributes = node.attrs.keys()
+ if not attributes:
+ continue
+
+ for attr in self.blacklisted_attributes:
+ if attr in attributes:
+ del node.attrs[attr]
+
+ self.strip_attributes_extra(node)
+
+ if 'href' in attributes:
+ protocol = node['href'].split(':')[0]
+ if protocol in self.blacklisted_protocols:
+ del node['href'] \ No newline at end of file
diff --git a/app/builder/views.py b/app/builder/views.py
new file mode 100644
index 0000000..af9bfaf
--- /dev/null
+++ b/app/builder/views.py
@@ -0,0 +1,37 @@
+from django.shortcuts import render
+from django.template import RequestContext
+#from builder.base import BuildWriting, BuildWritingFeed, BuildMap, BuildPhotos, BuildProjects, BuildSitemap
+#from jrnl.build import archive_builder, detail_builder, home_builder, rss_builder, map_builder
+from pages.build import BuildPages, BuildHome
+from posts.build import BuildJrnl, BuildFieldNotes, BuildSrc, BuildGuide
+#from lttr.build import lttr_builder
+
+def do_build(request):
+ section = request.GET.get('id', '')
+ context = {}
+ if section == 'builddetails':
+ context = {'message': 'Writing Jrnl Permalinks to Disk'}
+ p = BuildJrnl("posts", "post")
+ p.build_latest()
+ p.build_detail_view()
+ elif section == 'writingarchives':
+ context = {'message': 'Writing Jrnl Archives to Disk'}
+ BuildJrnl("posts", "post").build_arc()
+ elif section == 'buildrss':
+ context = {'message': 'Writing RSS to Disk'}
+ BuildJrnl("posts", "post").build_feed("jrnl:feed")
+ elif section == 'homepage':
+ context = {'message': 'Writing index to Disk'}
+ BuildHome("pages", "homepage").build()
+ elif section == 'pages':
+ context = {'message': 'Writing Pages to Disk'}
+ BuildPages("pages", "page", 'luxagraf.net').build()
+ elif section == 'lttr_archive':
+ context = {'message': 'Writing newsletter archives to Disk'}
+ #lttr_builder()
+ return render(request, 'admin/message.html', context)
+
+
+
+
+
diff --git a/app/classes/__init__.py b/app/classes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/classes/__init__.py
diff --git a/app/classes/admin.py b/app/classes/admin.py
new file mode 100644
index 0000000..5fcca4b
--- /dev/null
+++ b/app/classes/admin.py
@@ -0,0 +1,45 @@
+from django.contrib import admin
+from django import forms
+from django.contrib.gis.admin import OSMGeoAdmin
+from django.contrib.contenttypes.admin import GenericStackedInline
+
+from utils.widgets import AdminImageWidget, LGEntryForm
+from .models import Class, Session, ClassMedia
+
+
+@admin.register(Class)
+class ClassAdmin(admin.ModelAdmin):
+ list_display = ('title', 'session')
+ search_fields = ['title', 'description']
+ prepopulated_fields = {"slug": ('title',)}
+ list_filter = ('session',)
+ filter_horizontal = ('uploads',)
+
+ class Media:
+ js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
+
+
+@admin.register(Session)
+class SessionAdmin(admin.ModelAdmin):
+ list_display = ('title','date_start','date_end' )
+ prepopulated_fields = {"slug": ('title',)}
+
+ class Media:
+ js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
+
+
+@admin.register(ClassMedia)
+class ClassMediaAdmin(admin.ModelAdmin):
+ list_display = ('title','file' )
+
+ class Media:
+ js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
diff --git a/app/classes/migrations/0001_initial.py b/app/classes/migrations/0001_initial.py
new file mode 100644
index 0000000..b533aaf
--- /dev/null
+++ b/app/classes/migrations/0001_initial.py
@@ -0,0 +1,57 @@
+# Generated by Django 3.2.8 on 2022-01-09 19:25
+
+import classes.models
+import datetime
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ClassMedia',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200)),
+ ('date_created', models.DateTimeField(default=datetime.datetime.now)),
+ ('file', models.FileField(blank=True, null=True, upload_to=classes.models.get_upload_path)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Session',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200)),
+ ('date_start', models.DateField(verbose_name='Start Date')),
+ ('date_end', models.DateField(verbose_name='End Date')),
+ ('slug', models.SlugField()),
+ ],
+ options={
+ 'ordering': ('-date_start',),
+ 'get_latest_by': 'date_start',
+ },
+ ),
+ migrations.CreateModel(
+ name='Class',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200)),
+ ('description', models.TextField()),
+ ('requires', models.TextField(verbose_name='If previous classwork is required, list prerequisites here')),
+ ('slug', models.SlugField()),
+ ('date_created', models.DateTimeField(default=datetime.datetime.now)),
+ ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='classes.session')),
+ ('uploads', models.ManyToManyField(blank=True, to='classes.ClassMedia')),
+ ],
+ options={
+ 'ordering': ('-date_created',),
+ 'get_latest_by': 'date_created',
+ },
+ ),
+ ]
diff --git a/app/classes/migrations/0002_alter_class_requires.py b/app/classes/migrations/0002_alter_class_requires.py
new file mode 100644
index 0000000..8febb99
--- /dev/null
+++ b/app/classes/migrations/0002_alter_class_requires.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2022-01-09 19:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='class',
+ name='requires',
+ field=models.TextField(blank=True, null=True, verbose_name='If previous classwork is required, list prerequisites here'),
+ ),
+ ]
diff --git a/app/classes/migrations/0003_auto_20220109_2043.py b/app/classes/migrations/0003_auto_20220109_2043.py
new file mode 100644
index 0000000..cd25dcd
--- /dev/null
+++ b/app/classes/migrations/0003_auto_20220109_2043.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.8 on 2022-01-09 20:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0002_alter_class_requires'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='class',
+ name='number_of_classes',
+ field=models.IntegerField(default=8),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='class',
+ name='price',
+ field=models.FloatField(default=225),
+ preserve_default=False,
+ ),
+ ]
diff --git a/app/classes/migrations/0004_rename_name_class_title.py b/app/classes/migrations/0004_rename_name_class_title.py
new file mode 100644
index 0000000..00ac456
--- /dev/null
+++ b/app/classes/migrations/0004_rename_name_class_title.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2022-01-09 20:49
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0003_auto_20220109_2043'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='class',
+ old_name='name',
+ new_name='title',
+ ),
+ ]
diff --git a/app/classes/migrations/0005_auto_20220109_2100.py b/app/classes/migrations/0005_auto_20220109_2100.py
new file mode 100644
index 0000000..b5bd825
--- /dev/null
+++ b/app/classes/migrations/0005_auto_20220109_2100.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.8 on 2022-01-09 21:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0004_rename_name_class_title'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='classmedia',
+ old_name='name',
+ new_name='title',
+ ),
+ migrations.RenameField(
+ model_name='session',
+ old_name='name',
+ new_name='title',
+ ),
+ ]
diff --git a/app/classes/migrations/0006_auto_20220109_2104.py b/app/classes/migrations/0006_auto_20220109_2104.py
new file mode 100644
index 0000000..d25ead2
--- /dev/null
+++ b/app/classes/migrations/0006_auto_20220109_2104.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.2.8 on 2022-01-09 21:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0005_auto_20220109_2100'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='class',
+ name='class_days',
+ field=models.IntegerField(choices=[(0, 'Mon/Wed'), (1, 'Tue/Thur')], default=0),
+ ),
+ migrations.AddField(
+ model_name='session',
+ name='length',
+ field=models.PositiveIntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='class',
+ name='number_of_classes',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/app/classes/migrations/0007_session_status.py b/app/classes/migrations/0007_session_status.py
new file mode 100644
index 0000000..d01d944
--- /dev/null
+++ b/app/classes/migrations/0007_session_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2022-01-09 21:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0006_auto_20220109_2104'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='session',
+ name='status',
+ field=models.IntegerField(choices=[(0, 'Open'), (1, 'Closed')], default=0),
+ ),
+ ]
diff --git a/app/classes/migrations/0008_class_subtitle.py b/app/classes/migrations/0008_class_subtitle.py
new file mode 100644
index 0000000..1d572b0
--- /dev/null
+++ b/app/classes/migrations/0008_class_subtitle.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.8 on 2022-01-09 22:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('classes', '0007_session_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='class',
+ name='subtitle',
+ field=models.CharField(default='some text', max_length=200),
+ preserve_default=False,
+ ),
+ ]
diff --git a/app/classes/migrations/__init__.py b/app/classes/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/classes/migrations/__init__.py
diff --git a/app/classes/models.py b/app/classes/models.py
new file mode 100644
index 0000000..b10c863
--- /dev/null
+++ b/app/classes/models.py
@@ -0,0 +1,96 @@
+import datetime
+import os
+
+from django.dispatch import receiver
+from django.contrib.gis.db import models
+from django.db.models.signals import post_save
+from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.urls import reverse
+from django.utils.functional import cached_property
+from django.apps import apps
+from django.conf import settings
+from django.contrib.sitemaps import Sitemap
+from django import forms
+
+import urllib.request
+import urllib.parse
+import urllib.error
+from django_gravatar.helpers import get_gravatar_url, has_gravatar, calculate_gravatar_hash
+from django_comments.signals import comment_was_posted
+from django_comments.models import Comment
+from django_comments.moderation import CommentModerator, moderator
+
+from taggit.managers import TaggableManager
+
+from utils.util import render_images, render_products, parse_video, markdown_to_html, extract_main_image
+
+
+def get_upload_path(self, filename):
+ return "files/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+class Session(models.Model):
+ title = models.CharField(max_length=200)
+ date_start = models.DateField('Start Date')
+ date_end = models.DateField('End Date')
+ slug = models.SlugField()
+ length = models.PositiveIntegerField(blank=True, null=True)
+ STATUS = (
+ (0, 'Open'),
+ (1, 'Closed'),
+ )
+ status = models.IntegerField(choices=STATUS, default=0)
+
+ class Meta:
+ ordering = ('-date_start',)
+ get_latest_by = 'date_start'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse('classes:session-detail', kwargs={"year": self.start_date.year, "month": self.start_date.strftime("%m"), "slug": self.slug})
+
+ # save length from date
+
+
+class ClassMedia(models.Model):
+ title = models.CharField(max_length=200)
+ date_created = models.DateTimeField(default=datetime.datetime.now)
+ file = models.FileField(blank=True, null=True, upload_to=get_upload_path)
+
+ def __str__(self):
+ return self.title
+
+
+class Class(models.Model):
+ title = models.CharField(max_length=200)
+ subtitle = models.CharField(max_length=200)
+ description = models.TextField()
+ requires = models.TextField('If previous classwork is required, list prerequisites here', null=True, blank=True,)
+ session = models.ForeignKey(Session, blank=True, null=True, on_delete=models.SET_NULL)
+ slug = models.SlugField()
+ uploads = models.ManyToManyField(ClassMedia, blank=True)
+ date_created = models.DateTimeField(default=datetime.datetime.now)
+ number_of_classes = models.IntegerField(blank=True, null=True)
+ CLASS_DAYS = (
+ (0, 'Mon/Wed'),
+ (1, 'Tue/Thur'),
+ )
+ class_days = models.IntegerField(choices=CLASS_DAYS, default=0)
+ price = models.FloatField()
+
+ class Meta:
+ ordering = ('-date_created',)
+ get_latest_by = 'date_created'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse('classes:detail', kwargs={"slug": self.slug})
+
+ def save(self, *args, **kwargs):
+ super(Class, self).save(*args, **kwargs)
diff --git a/app/classes/templates/classes/class_detail.html b/app/classes/templates/classes/class_detail.html
new file mode 100644
index 0000000..50ae63f
--- /dev/null
+++ b/app/classes/templates/classes/class_detail.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block primary %}
+<main>
+<article>
+ <h2><a href="{{object.get_absolute_url}}">{{object.title|widont|smartypants}}</a></h2>
+ <h4>{{object.description|safe|widont|smartypants}}</h4>
+
+</article>
+</main>
+{% endblock %}
diff --git a/app/classes/templates/classes/class_list.html b/app/classes/templates/classes/class_list.html
new file mode 100644
index 0000000..fa1267c
--- /dev/null
+++ b/app/classes/templates/classes/class_list.html
@@ -0,0 +1,43 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load get_next %}
+{% load html5_datetime %}
+{% load pagination_tags %}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+<main role="main" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2 class="archive-hed">Classes</h2>
+ <p>More information coming soon.</p>
+ </div>
+ {% comment %}
+ {% autopaginate object_list 24 %}
+ <ul class="archive-list">{% for object in object_list %}
+ <li class="h-entry hentry archive-list-card" itemscope itemType="http://schema.org/Article">
+ {% if object.featured_image %}<a href="{{object.get_absolute_url}}" class="u-url">
+ <div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" class="u-photo" /></div>{%endif%}
+ <span class="date dt-published card-smcaps">{{object.pub_date|date:"F Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2 class="card-hed">{{object.title|safe|smartypants|widont}}</h2>
+ <p class="card-lede">{{object.subtitle}}</p>
+ <p class="p-summary">{{object.description}}</p>
+ {% if object.session.status == 0 %}
+ <p>Now Enrolling for {{object.session.title}} <br />({{object.number_of_classes}} classes every {{object.get_class_days_display}}, {{object.session.date_start}} - {{object.session.date_end}})</p>
+ {% else %}
+ <p>This class is not currently offered, but if you're interested please email me about setting up a future session.</p>
+ {% endif %}
+ {% if object.requires %}
+ <p>Prerequisites: {{object.requires}}</p>
+ {% endif %}
+ </a>
+ </li>
+ {%endfor%}</ul>
+ {%endcomment%}
+ </main>
+ {%comment%}
+ <nav aria-label="page navigation" class="pagination">
+ {% paginate %}
+ </nav>
+ {%endcomment%}
+</main>
+{% endblock %}
diff --git a/app/classes/urls.py b/app/classes/urls.py
new file mode 100644
index 0000000..ac9cce5
--- /dev/null
+++ b/app/classes/urls.py
@@ -0,0 +1,24 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "classes"
+
+urlpatterns = [
+ path(
+ r'<str:slug>',
+ views.ClassDetailView.as_view(),
+ name="detail"
+ ),
+ path(
+ r'',
+ views.ClassListView.as_view(),
+ {'page': 1},
+ name="list"
+ ),
+ path(
+ r'<page>/',
+ views.ClassListView.as_view(),
+ name="list"
+ ),
+]
diff --git a/app/classes/views.py b/app/classes/views.py
new file mode 100644
index 0000000..620e5db
--- /dev/null
+++ b/app/classes/views.py
@@ -0,0 +1,20 @@
+from utils.views import LuxDetailView
+from django.views.generic import DetailView, ListView
+from utils.views import PaginatedListView, LuxDetailView
+
+from .models import Class
+
+
+class ClassDetailView(LuxDetailView):
+ model = Class
+
+class ClassListView(PaginatedListView):
+ model = Class
+
+ def get_context_data(self, **kwargs):
+ '''
+ Add breadcrumb path
+ '''
+ context = super(ClassListView, self).get_context_data(**kwargs)
+ context['breadcrumbs'] = ("classes",)
+ return context
diff --git a/app/lib/contact/__init__.py b/app/lib/contact/__init__.py
new file mode 100644
index 0000000..e4c5943
--- /dev/null
+++ b/app/lib/contact/__init__.py
@@ -0,0 +1,104 @@
+from importlib import import_module
+
+from django.apps import apps
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+try:
+ from django.urls import reverse
+except ImportError:
+ from django.core.urlresolvers import reverse # Django < 1.10
+
+
+DEFAULT_COMMENTS_APP = 'django_comments'
+
+
+def get_comment_app():
+ """
+ Get the comment app (i.e. "django_comments") as defined in the settings
+ """
+ # Make sure the app's in INSTALLED_APPS
+ comments_app = get_comment_app_name()
+ if not apps.is_installed(comments_app):
+ raise ImproperlyConfigured(
+ "The COMMENTS_APP (%r) must be in INSTALLED_APPS" % comments_app
+ )
+
+ # Try to import the package
+ try:
+ package = import_module(comments_app)
+ except ImportError as e:
+ raise ImproperlyConfigured(
+ "The COMMENTS_APP setting refers to a non-existing package. (%s)" % e
+ )
+
+ return package
+
+
+def get_comment_app_name():
+ """
+ Returns the name of the comment app (either the setting value, if it
+ exists, or the default).
+ """
+ return getattr(settings, 'COMMENTS_APP', DEFAULT_COMMENTS_APP)
+
+
+def get_model():
+ """
+ Returns the comment model class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_model"):
+ return get_comment_app().get_model()
+ else:
+ from django_comments.models import Comment
+ return Comment
+
+
+def get_form():
+ from django_comments.forms import CommentForm
+ """
+ Returns the comment ModelForm class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form"):
+ return get_comment_app().get_form()
+ else:
+ return CommentForm
+
+
+def get_form_target():
+ """
+ Returns the target URL for the comment form submission view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form_target"):
+ return get_comment_app().get_form_target()
+ else:
+ return reverse("comments-post-comment")
+
+
+def get_flag_url(comment):
+ """
+ Get the URL for the "flag this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_flag_url"):
+ return get_comment_app().get_flag_url(comment)
+ else:
+ return reverse("comments-flag", args=(comment.id,))
+
+
+def get_delete_url(comment):
+ """
+ Get the URL for the "delete this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_delete_url"):
+ return get_comment_app().get_delete_url(comment)
+ else:
+ return reverse("comments-delete", args=(comment.id,))
+
+
+def get_approve_url(comment):
+ """
+ Get the URL for the "approve this comment from moderation" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_approve_url"):
+ return get_comment_app().get_approve_url(comment)
+ else:
+ return reverse("comments-approve", args=(comment.id,))
diff --git a/app/lib/contact/forms.py b/app/lib/contact/forms.py
new file mode 100644
index 0000000..cf905ac
--- /dev/null
+++ b/app/lib/contact/forms.py
@@ -0,0 +1,145 @@
+"""
+A base contact form for allowing users to send email messages through
+a web interface.
+
+"""
+
+from typing import Any, Dict, List, Optional
+
+from django import forms, http
+from django.conf import settings
+from django.contrib.sites.shortcuts import get_current_site
+from django.core.mail import send_mail
+from django.template import loader
+from django.utils.translation import gettext_lazy as _
+
+# Parameters to a form being handled from a live request will actually
+# be instances of django.utils.datastructures.MultiValueDict, but this
+# is declared as a typing.Dict for a few reasons:
+#
+# * MultiValueDict is a subclass of dict, so instances of
+# MultiValueDict will pass type checks.
+#
+# * Testing of forms more typically passes in plain dict for the form
+# arguments, so supporting it is useful.
+#
+# * PEP 560, which allows dropping the typing module's aliases and
+# subscripting the actual types, was adopted for Python 3.7, but
+# currently we support back to Python 3.5.
+StringKeyedDict = Dict[str, Any]
+
+
+class ContactForm(forms.Form):
+ """
+ The base contact form class from which all contact form classes
+ should inherit.
+
+ """
+
+ name = forms.CharField(max_length=100, label=_("Your name"))
+ email = forms.EmailField(max_length=200, label=_("Your email address"))
+ body = forms.CharField(widget=forms.Textarea, label=_("Your message"))
+
+ from_email = settings.DEFAULT_FROM_EMAIL
+
+ recipient_list = [mail_tuple[1] for mail_tuple in settings.MANAGERS]
+
+ subject_template_name = "contact/contact_form_subject.txt"
+
+ template_name = "contact/contact_form.txt"
+
+ def __init__(
+ self,
+ data: Optional[StringKeyedDict] = None,
+ files: Optional[StringKeyedDict] = None,
+ request: Optional[http.HttpRequest] = None,
+ recipient_list: Optional[List[str]] = None,
+ *args,
+ **kwargs
+ ):
+ if request is None:
+ raise TypeError("Keyword argument 'request' must be supplied")
+ self.request = request
+ if recipient_list is not None:
+ self.recipient_list = recipient_list
+ super().__init__(data=data, files=files, *args, **kwargs)
+
+ def message(self) -> str:
+ """
+ Render the body of the message to a string.
+
+ """
+ template_name = (
+ self.template_name() if callable(self.template_name) else self.template_name
+ )
+ return loader.render_to_string(
+ template_name, self.get_context(), request=self.request
+ )
+
+ def subject(self) -> str:
+ """
+ Render the subject of the message to a string.
+
+ """
+ template_name = (
+ self.subject_template_name()
+ if callable(self.subject_template_name)
+ else self.subject_template_name
+ )
+ subject = loader.render_to_string(
+ template_name, self.get_context(), request=self.request
+ )
+ return "".join(subject.splitlines())
+
+ def get_context(self) -> StringKeyedDict:
+ """
+ Return the context used to render the templates for the email
+ subject and body.
+
+ By default, this context includes:
+
+ * All of the validated values in the form, as variables of the
+ same names as their fields.
+
+ * The current ``Site`` object, as the variable ``site``.
+
+ * Any additional variables added by context processors (this
+ will be a ``RequestContext``).
+
+ """
+ if not self.is_valid():
+ raise ValueError("Cannot generate Context from invalid contact form")
+ return dict(self.cleaned_data, site=get_current_site(self.request))
+
+ def get_message_dict(self) -> StringKeyedDict:
+ """
+ Generate the various parts of the message and return them in a
+ dictionary, suitable for passing directly as keyword arguments
+ to ``django.core.mail.send_mail()``.
+
+ By default, the following values are returned:
+
+ * ``from_email``
+
+ * ``message``
+
+ * ``recipient_list``
+
+ * ``subject``
+
+ """
+ if not self.is_valid():
+ raise ValueError("Message cannot be sent from invalid contact form")
+ message_dict = {}
+ for message_part in ("from_email", "message", "recipient_list", "subject"):
+ attr = getattr(self, message_part)
+ message_dict[message_part] = attr() if callable(attr) else attr
+ return message_dict
+
+ def save(self, fail_silently: bool = False) -> None:
+ """
+ Build and send the email message.
+
+ """
+ send_mail(fail_silently=fail_silently, **self.get_message_dict())
+
diff --git a/app/lib/contact/templates/contact/contact_form.html b/app/lib/contact/templates/contact/contact_form.html
new file mode 100644
index 0000000..1be2c27
--- /dev/null
+++ b/app/lib/contact/templates/contact/contact_form.html
@@ -0,0 +1,28 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{%block bodyid%}id="{{object.title|slugify}}"{%endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+<main role="main" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2 class="archive-hed">Contact Information</h2>
+ <p>Want to know more about private tutoring or group classes? Email me: <a href="mailto:corrinne@cumuluslearning.net">corrinne@cumuluslearning.net</a></p>
+
+ <p>Or you can use the form below to get in touch.</p>
+ <form action="" method="post" class="comment-form contact-form card-subscribe">{% csrf_token %}
+ {% for field in form %}
+ <fieldset>
+ {{field.label_tag}}
+ {%if field.name == "body"%}<div class="textarea-rounded">{{ field }}</div>{%else%}{{field}}{%endif%}
+ </fieldset>
+ {% if forloop.last %}<input type="submit" name="post" class="btn" value="Send" />{%endif%}
+ <small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small>
+ {%endfor%}
+ </form>
+ </div>
+ </main>
+</main>
+{% endblock %}
+
+{{body}}
diff --git a/app/lib/contact/templates/contact/contact_form.txt b/app/lib/contact/templates/contact/contact_form.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/contact/templates/contact/contact_form.txt
diff --git a/app/lib/contact/templates/contact/contact_form_sent.html b/app/lib/contact/templates/contact/contact_form_sent.html
new file mode 100644
index 0000000..668224e
--- /dev/null
+++ b/app/lib/contact/templates/contact/contact_form_sent.html
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% block primary %}
+<main role="main" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2 class="archive-hed">Thank You</h2>
+ <p>I'll get back to you very soon.</p>
+ </div>
+ </main>
+</main>
+{% endblock %}
+
+{{body}}
diff --git a/app/lib/contact/templates/contact/contact_form_subject.txt b/app/lib/contact/templates/contact/contact_form_subject.txt
new file mode 100644
index 0000000..498c81f
--- /dev/null
+++ b/app/lib/contact/templates/contact/contact_form_subject.txt
@@ -0,0 +1 @@
+[Cumulus Learning]
diff --git a/app/lib/contact/urls.py b/app/lib/contact/urls.py
new file mode 100644
index 0000000..080b33c
--- /dev/null
+++ b/app/lib/contact/urls.py
@@ -0,0 +1,22 @@
+"""
+Example URLConf for a contact form.
+
+If all you want is the basic ContactForm with default behavior,
+include this URLConf somewhere in your URL hierarchy (for example, at
+``/contact/``)
+
+"""
+
+from django.urls import path
+from django.views.generic import TemplateView
+
+from contact.views import ContactFormView
+
+urlpatterns = [
+ path("", ContactFormView.as_view(), name="contact_form"),
+ path(
+ "sent/",
+ TemplateView.as_view(template_name="contact/contact_form_sent.html"),
+ name="contact_form_sent",
+ ),
+]
diff --git a/app/lib/contact/views.py b/app/lib/contact/views.py
new file mode 100644
index 0000000..9e2c9c7
--- /dev/null
+++ b/app/lib/contact/views.py
@@ -0,0 +1,45 @@
+"""
+View which can render and send email from a contact form.
+
+"""
+
+from django import http
+from django.urls import reverse_lazy
+from django.views.generic.edit import FormView
+
+from .forms import ContactForm, StringKeyedDict
+
+
+class ContactFormView(FormView):
+ form_class = ContactForm
+ recipient_list = None
+ success_url = reverse_lazy("contact_form_sent")
+ template_name = "contact/contact_form.html"
+
+ def get_context_data(self, **kwargs):
+ '''
+ Adds breadcrumb path to every view
+ '''
+ # Call the base implementation first to get a context
+
+ context = super().get_context_data(**kwargs)
+ # special case for pages:
+ context['breadcrumbs'] = ('contact',)
+ context['crumb_url'] = None
+ return context
+
+ def form_valid(self, form) -> http.HttpResponse:
+ form.save()
+ return super().form_valid(form)
+
+ def get_form_kwargs(self) -> StringKeyedDict:
+ # ContactForm instances require instantiation with an
+ # HttpRequest.
+ kwargs = super().get_form_kwargs()
+ kwargs.update({"request": self.request})
+
+ # We may also have been given a recipient list when
+ # instantiated.
+ if self.recipient_list is not None:
+ kwargs.update({"recipient_list": self.recipient_list})
+ return kwargs
diff --git a/app/lib/django_comments/__init__.py b/app/lib/django_comments/__init__.py
new file mode 100644
index 0000000..e4c5943
--- /dev/null
+++ b/app/lib/django_comments/__init__.py
@@ -0,0 +1,104 @@
+from importlib import import_module
+
+from django.apps import apps
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+try:
+ from django.urls import reverse
+except ImportError:
+ from django.core.urlresolvers import reverse # Django < 1.10
+
+
+DEFAULT_COMMENTS_APP = 'django_comments'
+
+
+def get_comment_app():
+ """
+ Get the comment app (i.e. "django_comments") as defined in the settings
+ """
+ # Make sure the app's in INSTALLED_APPS
+ comments_app = get_comment_app_name()
+ if not apps.is_installed(comments_app):
+ raise ImproperlyConfigured(
+ "The COMMENTS_APP (%r) must be in INSTALLED_APPS" % comments_app
+ )
+
+ # Try to import the package
+ try:
+ package = import_module(comments_app)
+ except ImportError as e:
+ raise ImproperlyConfigured(
+ "The COMMENTS_APP setting refers to a non-existing package. (%s)" % e
+ )
+
+ return package
+
+
+def get_comment_app_name():
+ """
+ Returns the name of the comment app (either the setting value, if it
+ exists, or the default).
+ """
+ return getattr(settings, 'COMMENTS_APP', DEFAULT_COMMENTS_APP)
+
+
+def get_model():
+ """
+ Returns the comment model class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_model"):
+ return get_comment_app().get_model()
+ else:
+ from django_comments.models import Comment
+ return Comment
+
+
+def get_form():
+ from django_comments.forms import CommentForm
+ """
+ Returns the comment ModelForm class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form"):
+ return get_comment_app().get_form()
+ else:
+ return CommentForm
+
+
+def get_form_target():
+ """
+ Returns the target URL for the comment form submission view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form_target"):
+ return get_comment_app().get_form_target()
+ else:
+ return reverse("comments-post-comment")
+
+
+def get_flag_url(comment):
+ """
+ Get the URL for the "flag this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_flag_url"):
+ return get_comment_app().get_flag_url(comment)
+ else:
+ return reverse("comments-flag", args=(comment.id,))
+
+
+def get_delete_url(comment):
+ """
+ Get the URL for the "delete this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_delete_url"):
+ return get_comment_app().get_delete_url(comment)
+ else:
+ return reverse("comments-delete", args=(comment.id,))
+
+
+def get_approve_url(comment):
+ """
+ Get the URL for the "approve this comment from moderation" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_approve_url"):
+ return get_comment_app().get_approve_url(comment)
+ else:
+ return reverse("comments-approve", args=(comment.id,))
diff --git a/app/lib/django_comments/abstracts.py b/app/lib/django_comments/abstracts.py
new file mode 100644
index 0000000..e74ea02
--- /dev/null
+++ b/app/lib/django_comments/abstracts.py
@@ -0,0 +1,183 @@
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.apps import apps
+from django.utils.html import mark_safe
+from django.db import models
+from django.utils import timezone
+from six import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+try:
+ from django.urls import reverse
+except ImportError:
+ from django.core.urlresolvers import reverse # Django < 1.10
+
+from .managers import CommentManager
+
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+
+
+class BaseCommentAbstractModel(models.Model):
+ """
+ An abstract base class that any custom comment models probably should
+ subclass.
+ """
+
+ # Content-object field
+ content_type = models.ForeignKey(ContentType,
+ verbose_name=_('content type'),
+ related_name="content_type_set_for_%(class)s",
+ on_delete=models.CASCADE)
+ object_pk = models.TextField(_('object ID'))
+ content_object = GenericForeignKey(ct_field="content_type", fk_field="object_pk")
+
+ # Metadata about the comment
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+
+ class Meta:
+ abstract = True
+
+ def get_content_object_url(self):
+ """
+ Get a URL suitable for redirecting to the content object.
+ """
+ return reverse(
+ "comments-url-redirect",
+ args=(self.content_type_id, self.object_pk)
+ )
+
+ def get_object_url(self):
+ ctype = self.content_type
+ object_pk = self.object_pk
+ model = apps.get_model(ctype.app_label, ctype.model)
+ target = model.objects.get(pk=object_pk)
+ return mark_safe("<a href='%s'>%s</a>" % (target.get_absolute_url(), target.title))
+ get_object_url.short_description = "comment item"
+
+@python_2_unicode_compatible
+class CommentAbstractModel(BaseCommentAbstractModel):
+ """
+ A user comment about some object.
+ """
+
+ # Who posted this comment? If ``user`` is set then it was an authenticated
+ # user; otherwise at least user_name should have been set and the comment
+ # was posted by a non-authenticated user.
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
+ blank=True, null=True, related_name="%(class)s_comments",
+ on_delete=models.SET_NULL)
+ user_name = models.CharField(_("user's name"), max_length=50, blank=True)
+ # Explicit `max_length` to apply both to Django 1.7 and 1.8+.
+ user_email = models.EmailField(_("user's email address"), max_length=254,
+ blank=True)
+ user_url = models.URLField(_("user's URL"), blank=True)
+
+ comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
+
+ # Metadata about the comment
+ submit_date = models.DateTimeField(_('date/time submitted'), default=None, db_index=True)
+ ip_address = models.GenericIPAddressField(_('IP address'), unpack_ipv4=True, blank=True, null=True)
+ is_public = models.BooleanField(_('is public'), default=True,
+ help_text=_('Uncheck this box to make the comment effectively '
+ 'disappear from the site.'))
+ is_removed = models.BooleanField(_('is removed'), default=False,
+ help_text=_('Check this box if the comment is inappropriate. '
+ 'A "This comment has been removed" message will '
+ 'be displayed instead.'))
+
+ # Manager
+ objects = CommentManager()
+
+ class Meta:
+ abstract = True
+ ordering = ('submit_date',)
+ permissions = [("can_moderate", "Can moderate comments")]
+ verbose_name = _('comment')
+ verbose_name_plural = _('comments')
+
+ def __str__(self):
+ return "%s: %s..." % (self.name, self.comment[:50])
+
+ def save(self, *args, **kwargs):
+ if self.submit_date is None:
+ self.submit_date = timezone.now()
+ super(CommentAbstractModel, self).save(*args, **kwargs)
+
+ def _get_userinfo(self):
+ """
+ Get a dictionary that pulls together information about the poster
+ safely for both authenticated and non-authenticated comments.
+
+ This dict will have ``name``, ``email``, and ``url`` fields.
+ """
+ if not hasattr(self, "_userinfo"):
+ userinfo = {
+ "name": self.user_name,
+ "email": self.user_email,
+ "url": self.user_url
+ }
+ if self.user_id:
+ u = self.user
+ if u.email:
+ userinfo["email"] = u.email
+
+ # If the user has a full name, use that for the user name.
+ # However, a given user_name overrides the raw user.username,
+ # so only use that if this comment has no associated name.
+ if u.get_full_name():
+ userinfo["name"] = self.user.get_full_name()
+ elif not self.user_name:
+ userinfo["name"] = u.get_username()
+ self._userinfo = userinfo
+ return self._userinfo
+
+ userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
+
+ def _get_name(self):
+ return self.userinfo["name"]
+
+ def _set_name(self, val):
+ if self.user_id:
+ raise AttributeError(_("This comment was posted by an authenticated "
+ "user and thus the name is read-only."))
+ self.user_name = val
+
+ name = property(_get_name, _set_name, doc="The name of the user who posted this comment")
+
+ def _get_email(self):
+ return self.userinfo["email"]
+
+ def _set_email(self, val):
+ if self.user_id:
+ raise AttributeError(_("This comment was posted by an authenticated "
+ "user and thus the email is read-only."))
+ self.user_email = val
+
+ email = property(_get_email, _set_email, doc="The email of the user who posted this comment")
+
+ def _get_url(self):
+ return self.userinfo["url"]
+
+ def _set_url(self, val):
+ self.user_url = val
+
+ url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
+
+ def get_absolute_url(self, anchor_pattern="#c%(id)s"):
+ return self.get_content_object_url() + (anchor_pattern % self.__dict__)
+
+ def get_as_text(self):
+ """
+ Return this comment as plain text. Useful for emails.
+ """
+ d = {
+ 'user': self.user or self.name,
+ 'date': self.submit_date,
+ 'comment': self.comment,
+ 'domain': self.site.domain,
+ 'url': self.get_absolute_url()
+ }
+ return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
diff --git a/app/lib/django_comments/admin.py b/app/lib/django_comments/admin.py
new file mode 100644
index 0000000..8451c70
--- /dev/null
+++ b/app/lib/django_comments/admin.py
@@ -0,0 +1,95 @@
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.utils.translation import ugettext_lazy as _, ungettext
+
+from django_comments import get_model
+from django_comments.views.moderation import perform_flag, perform_approve, perform_delete
+
+
+class UsernameSearch(object):
+ """The User object may not be auth.User, so we need to provide
+ a mechanism for issuing the equivalent of a .filter(user__username=...)
+ search in CommentAdmin.
+ """
+
+ def __str__(self):
+ return 'user__%s' % get_user_model().USERNAME_FIELD
+
+
+class CommentsAdmin(admin.ModelAdmin):
+
+ fieldsets = (
+ (
+ None,
+ {'fields': ('content_type', 'object_pk', 'site')}
+ ),
+ (
+ _('Content'),
+ {'fields': ('user', 'user_name', 'user_email', 'user_url', 'comment')}
+ ),
+ (
+ _('Metadata'),
+ {'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')}
+ ),
+ )
+
+ list_display = ('name', 'content_type', 'get_object_url', 'object_pk', 'ip_address', 'submit_date', 'is_public', 'is_removed')
+ list_filter = ('submit_date', 'site', 'is_public', 'is_removed')
+ date_hierarchy = 'submit_date'
+ ordering = ('-submit_date',)
+ raw_id_fields = ('user',)
+ search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address')
+ actions = ["flag_comments", "approve_comments", "remove_comments"]
+
+ def get_actions(self, request):
+ actions = super(CommentsAdmin, self).get_actions(request)
+ # Only superusers should be able to delete the comments from the DB.
+ if not request.user.is_superuser and 'delete_selected' in actions:
+ actions.pop('delete_selected')
+ if not request.user.has_perm('django_comments.can_moderate'):
+ if 'approve_comments' in actions:
+ actions.pop('approve_comments')
+ if 'remove_comments' in actions:
+ actions.pop('remove_comments')
+ return actions
+
+ def flag_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_flag,
+ lambda n: ungettext('flagged', 'flagged', n))
+
+ flag_comments.short_description = _("Flag selected comments")
+
+ def approve_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_approve,
+ lambda n: ungettext('approved', 'approved', n))
+
+ approve_comments.short_description = _("Approve selected comments")
+
+ def remove_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_delete,
+ lambda n: ungettext('removed', 'removed', n))
+
+ remove_comments.short_description = _("Remove selected comments")
+
+ def _bulk_flag(self, request, queryset, action, done_message):
+ """
+ Flag, approve, or remove some comments from an admin action. Actually
+ calls the `action` argument to perform the heavy lifting.
+ """
+ n_comments = 0
+ for comment in queryset:
+ action(request, comment)
+ n_comments += 1
+
+ msg = ungettext('%(count)s comment was successfully %(action)s.',
+ '%(count)s comments were successfully %(action)s.',
+ n_comments)
+ self.message_user(request, msg % {'count': n_comments, 'action': done_message(n_comments)})
+
+# Only register the default admin if the model is the built-in comment model
+# (this won't be true if there's a custom comment app).
+Klass = get_model()
+if Klass._meta.app_label == "django_comments":
+ admin.site.register(Klass, CommentsAdmin)
diff --git a/app/lib/django_comments/compat.py b/app/lib/django_comments/compat.py
new file mode 100644
index 0000000..7b7d56d
--- /dev/null
+++ b/app/lib/django_comments/compat.py
@@ -0,0 +1,3 @@
+"""
+Module to store compatiblity imports to prevent Django deprecation warnings.
+"""
diff --git a/app/lib/django_comments/feeds.py b/app/lib/django_comments/feeds.py
new file mode 100644
index 0000000..bc8c501
--- /dev/null
+++ b/app/lib/django_comments/feeds.py
@@ -0,0 +1,33 @@
+from django.contrib.sites.shortcuts import get_current_site
+from django.contrib.syndication.views import Feed
+from django.utils.translation import ugettext as _
+
+import django_comments
+
+
+class LatestCommentFeed(Feed):
+ """Feed of latest comments on the current site."""
+
+ def __call__(self, request, *args, **kwargs):
+ self.site = get_current_site(request)
+ return super(LatestCommentFeed, self).__call__(request, *args, **kwargs)
+
+ def title(self):
+ return _("%(site_name)s comments") % dict(site_name=self.site.name)
+
+ def link(self):
+ return "http://%s/" % (self.site.domain)
+
+ def description(self):
+ return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name)
+
+ def items(self):
+ qs = django_comments.get_model().objects.filter(
+ site__pk=self.site.pk,
+ is_public=True,
+ is_removed=False,
+ )
+ return qs.order_by('-submit_date')[:40]
+
+ def item_pubdate(self, item):
+ return item.submit_date
diff --git a/app/lib/django_comments/forms.py b/app/lib/django_comments/forms.py
new file mode 100644
index 0000000..7b7eafd
--- /dev/null
+++ b/app/lib/django_comments/forms.py
@@ -0,0 +1,200 @@
+import time
+
+from django import forms
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.forms.utils import ErrorDict
+from django.utils.crypto import salted_hmac, constant_time_compare
+from django.utils.encoding import force_text
+from django.utils.text import get_text_list
+from django.utils import timezone
+from django.utils.translation import pgettext_lazy, ungettext, ugettext, ugettext_lazy as _
+
+from . import get_model
+
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+DEFAULT_COMMENTS_TIMEOUT = getattr(settings, 'COMMENTS_TIMEOUT', (2 * 60 * 60)) # 2h
+
+
+class CommentSecurityForm(forms.Form):
+ """
+ Handles the security aspects (anti-spoofing) for comment forms.
+ """
+ content_type = forms.CharField(widget=forms.HiddenInput)
+ object_pk = forms.CharField(widget=forms.HiddenInput)
+ timestamp = forms.IntegerField(widget=forms.HiddenInput)
+ security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)
+
+ def __init__(self, target_object, data=None, initial=None, **kwargs):
+ self.target_object = target_object
+ if initial is None:
+ initial = {}
+ initial.update(self.generate_security_data())
+ super(CommentSecurityForm, self).__init__(data=data, initial=initial, **kwargs)
+
+ def security_errors(self):
+ """Return just those errors associated with security"""
+ errors = ErrorDict()
+ for f in ["honeypot", "timestamp", "security_hash"]:
+ if f in self.errors:
+ errors[f] = self.errors[f]
+ return errors
+
+ def clean_security_hash(self):
+ """Check the security hash."""
+ security_hash_dict = {
+ 'content_type': self.data.get("content_type", ""),
+ 'object_pk': self.data.get("object_pk", ""),
+ 'timestamp': self.data.get("timestamp", ""),
+ }
+ expected_hash = self.generate_security_hash(**security_hash_dict)
+ actual_hash = self.cleaned_data["security_hash"]
+ if not constant_time_compare(expected_hash, actual_hash):
+ raise forms.ValidationError("Security hash check failed.")
+ return actual_hash
+
+ def clean_timestamp(self):
+ """Make sure the timestamp isn't too far (default is > 2 hours) in the past."""
+ ts = self.cleaned_data["timestamp"]
+ return ts
+
+ def generate_security_data(self):
+ """Generate a dict of security data for "initial" data."""
+ timestamp = int(time.time())
+ security_dict = {
+ 'content_type': str(self.target_object._meta),
+ 'object_pk': str(self.target_object._get_pk_val()),
+ 'timestamp': str(timestamp),
+ 'security_hash': self.initial_security_hash(timestamp),
+ }
+ return security_dict
+
+ def initial_security_hash(self, timestamp):
+ """
+ Generate the initial security hash from self.content_object
+ and a (unix) timestamp.
+ """
+
+ initial_security_dict = {
+ 'content_type': str(self.target_object._meta),
+ 'object_pk': str(self.target_object._get_pk_val()),
+ 'timestamp': str(timestamp),
+ }
+ return self.generate_security_hash(**initial_security_dict)
+
+ def generate_security_hash(self, content_type, object_pk, timestamp):
+ """
+ Generate a HMAC security hash from the provided info.
+ """
+ info = (content_type, object_pk, timestamp)
+ key_salt = "django.contrib.forms.CommentSecurityForm"
+ value = "-".join(info)
+ return salted_hmac(key_salt, value).hexdigest()
+
+
+class CommentDetailsForm(CommentSecurityForm):
+ """
+ Handles the specific details of the comment (name, comment, etc.).
+ """
+ name = forms.CharField(label=pgettext_lazy("Person name", "Name"), max_length=50)
+ email = forms.EmailField(label=_("Email address"))
+ url = forms.URLField(label=_("URL"), required=False)
+ # Translators: 'Comment' is a noun here.
+ comment = forms.CharField(label=_('Comment'), widget=forms.Textarea,
+ max_length=COMMENT_MAX_LENGTH)
+
+ def get_comment_object(self, site_id=None):
+ """
+ Return a new (unsaved) comment object based on the information in this
+ form. Assumes that the form is already validated and will throw a
+ ValueError if not.
+
+ Does not set any of the fields that would come from a Request object
+ (i.e. ``user`` or ``ip_address``).
+ """
+ if not self.is_valid():
+ raise ValueError("get_comment_object may only be called on valid forms")
+
+ CommentModel = self.get_comment_model()
+ new = CommentModel(**self.get_comment_create_data(site_id=site_id))
+ new = self.check_for_duplicate_comment(new)
+
+ return new
+
+ def get_comment_model(self):
+ """
+ Get the comment model to create with this form. Subclasses in custom
+ comment apps should override this, get_comment_create_data, and perhaps
+ check_for_duplicate_comment to provide custom comment models.
+ """
+ return get_model()
+
+ def get_comment_create_data(self, site_id=None):
+ """
+ Returns the dict of data to be used to create a comment. Subclasses in
+ custom comment apps that override get_comment_model can override this
+ method to add extra fields onto a custom comment model.
+ """
+ return dict(
+ content_type=ContentType.objects.get_for_model(self.target_object),
+ object_pk=force_text(self.target_object._get_pk_val()),
+ user_name=self.cleaned_data["name"],
+ user_email=self.cleaned_data["email"],
+ user_url=self.cleaned_data["url"],
+ comment=self.cleaned_data["comment"],
+ submit_date=timezone.now(),
+ site_id=site_id or getattr(settings, "SITE_ID", None),
+ is_public=True,
+ is_removed=False,
+ )
+
+ def check_for_duplicate_comment(self, new):
+ """
+ Check that a submitted comment isn't a duplicate. This might be caused
+ by someone posting a comment twice. If it is a dup, silently return the *previous* comment.
+ """
+ possible_duplicates = self.get_comment_model()._default_manager.using(
+ self.target_object._state.db
+ ).filter(
+ content_type=new.content_type,
+ object_pk=new.object_pk,
+ user_name=new.user_name,
+ user_email=new.user_email,
+ user_url=new.user_url,
+ )
+ for old in possible_duplicates:
+ if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
+ return old
+
+ return new
+
+ def clean_comment(self):
+ """
+ If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
+ contain anything in PROFANITIES_LIST.
+ """
+ comment = self.cleaned_data["comment"]
+ if (not getattr(settings, 'COMMENTS_ALLOW_PROFANITIES', False) and
+ getattr(settings, 'PROFANITIES_LIST', False)):
+ bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()]
+ if bad_words:
+ raise forms.ValidationError(ungettext(
+ "Watch your mouth! The word %s is not allowed here.",
+ "Watch your mouth! The words %s are not allowed here.",
+ len(bad_words)) % get_text_list(
+ ['"%s%s%s"' % (i[0], '-' * (len(i) - 2), i[-1])
+ for i in bad_words], ugettext('and')))
+ return comment
+
+
+class CommentForm(CommentDetailsForm):
+ honeypot = forms.CharField(required=False,
+ label=_('If you enter anything in this field '
+ 'your comment will be treated as spam'))
+
+ def clean_honeypot(self):
+ """Check that nothing's been entered into the honeypot."""
+ value = self.cleaned_data["honeypot"]
+ if value:
+ raise forms.ValidationError(self.fields["honeypot"].label)
+ return value
diff --git a/app/lib/django_comments/locale/ar/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ar/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..f772ff3
--- /dev/null
+++ b/app/lib/django_comments/locale/ar/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ar/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4046e24
--- /dev/null
+++ b/app/lib/django_comments/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,322 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Bashar Al-Abdulhadi, 2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Ossama Khayat <okhayat@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-04-03 08:20+0000\n"
+"Last-Translator: Bashar Al-Abdulhadi\n"
+"Language-Team: Arabic (http://www.transifex.com/django/django-contrib-comments/language/ar/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ar\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "محتوى"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "ميتاداتا"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "لا يوجد"
+msgstr[1] "عليه علامة"
+msgstr[2] "عليهما علامة"
+msgstr[3] "عليها علامة"
+msgstr[4] "عليها علامة"
+msgstr[5] "عليها علامة"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "تعليم التعليقات المحددة"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "اعتُمد"
+msgstr[1] "اعتُمد"
+msgstr[2] "اعتُمدا"
+msgstr[3] "اعتُمدت"
+msgstr[4] "اعتُمدت"
+msgstr[5] "اعتُمدت"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "اعتماد التعليقات المحددة"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "أزيل"
+msgstr[1] "أزيل"
+msgstr[2] "أزيلا"
+msgstr[3] "أزيلت"
+msgstr[4] "أزيل"
+msgstr[5] "أزيل"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "احذف التعليقات المحددة"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "تعليق %(count)s %(action)s."
+msgstr[1] "تعليق %(count)s %(action)s."
+msgstr[2] "تعليقان %(count)s %(action)s."
+msgstr[3] "تعليقات %(count)s %(action)s."
+msgstr[4] "تعليق %(count)s %(action)s."
+msgstr[5] "تعليقات %(count)s %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "تعليقات %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "آخر التعليقات على %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "الاسم"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "عنوان بريد إلكتروني"
+
+#: forms.py:98
+msgid "URL"
+msgstr "رابط"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "تعليق"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+msgstr[1] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+msgstr[2] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+msgstr[3] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+msgstr[4] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+msgstr[5] "احفظ لسانك! الكلمة %s ممنوعة هنا."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "و"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "إن كتبت أي شيء في هذا الحقل فسيُعتبر تعليقك غير مرغوب به"
+
+#: models.py:23
+msgid "content type"
+msgstr "نوع البيانات"
+
+#: models.py:25
+msgid "object ID"
+msgstr "معرف العنصر"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "مستخدم"
+
+#: models.py:55
+msgid "user's name"
+msgstr "اسم المستخدم"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "عنوان البريد الإلكتروني للمستخدم"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "عنوان URL للمستخدم"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "تعليق"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "تاريخ ووقت الإرسال"
+
+#: models.py:63
+msgid "IP address"
+msgstr "عنوان IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "عام"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "أزل اختيار هذا المربّع كي تُزيل التعليق نهائياً من الموقع."
+
+#: models.py:67
+msgid "is removed"
+msgstr "محذوف"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "انتق هذا المربع إذا كان التعليق غير لائق، سيتم عرض الرسالة \"تم حذف هذا التعليق\" بدلا منه."
+
+#: models.py:80
+msgid "comments"
+msgstr "تعليقات"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "كتب هذا التعليق مستخدم مُسجّل ولذا كان اسمهللقراءة فقط."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "كتب هذا التعليق مستخدم مُسجّل ولذا كان عنوان بريده الالكتروني للقراءة فقط."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "كتبه %(user)s في %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "علم"
+
+#: models.py:180
+msgid "date"
+msgstr "التاريخ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "علَم التعليق"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "أعلام التعليقات"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] تم نشر تعليق جديد على \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "وافق على تعليق"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "تريد فعلاً جعل هذا التعليق عامّاً؟"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "وافق"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "شكراً لموافقتك"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "شكراً لك على قضاء وقتك في تحسين جودة النقاش على موقعنا"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "أزل تعليق"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "تريد فعلاً إزالة هذا التعليق؟"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "أزل"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "شكراً لإزالته"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "سِم هذا التعليق"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "تريد فعلاً وسم هذا التعليق؟"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "سٍم"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "شكراً لك على الوَسم"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "أرسل "
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "عاين"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "شكراً على تعليقك"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "شكراً لك على تعليقك"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "عاين تعليقك"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "رجاءً صحِّح الخطأ أدناه"
+msgstr[1] "رجاءً صحِّح الخطأ أدناه"
+msgstr[2] "رجاءً صحِّح الأخطاء أدناه"
+msgstr[3] "رجاءً صحِّح الأخطاء أدناه"
+msgstr[4] "رجاءً صحِّح الأخطاء أدناه"
+msgstr[5] "رجاءً صحِّح الأخطاء أدناه"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "أرسال تعليقك"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "أو قم ببعض التغيير"
diff --git a/app/lib/django_comments/locale/az/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/az/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e747751
--- /dev/null
+++ b/app/lib/django_comments/locale/az/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/az/LC_MESSAGES/django.po b/app/lib/django_comments/locale/az/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4f4c8d3
--- /dev/null
+++ b/app/lib/django_comments/locale/az/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Ali Ismayilov <ali@ismailov.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Azerbaijani (http://www.transifex.com/django/django-contrib-comments/language/az/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: az\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Məzmun"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meta-məlumat"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Seçilmiş şərhləri işarələ"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Seçilmiş şərhləri təsdiq et"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Seçilmiş şərhləri sil"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s şərhləri"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s üzrə son şərhlər"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-poçt"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Şərh"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "və"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Bu sahəyə nəsə yazsanız, şərhiniz spam kimi qiymətləndiriləcək."
+
+#: models.py:23
+msgid "content type"
+msgstr "məzmunun tipi"
+
+#: models.py:25
+msgid "object ID"
+msgstr "obyektin ID-si"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "istifadəçi"
+
+#: models.py:55
+msgid "user's name"
+msgstr "istifadəçinin adı"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "istifadəçinin e-poçt ünvanı"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "istifadəçinin URL-ni"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "şərh"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "yazılma tarix/vaxtı"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP ünvanı"
+
+#: models.py:64
+msgid "is public"
+msgstr "hamı görür"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Şərhi saytdan yox etmək üçün buradakı quşu yığışdırın."
+
+#: models.py:67
+msgid "is removed"
+msgstr "yığışdırılıb"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Əgər şərh qeyri-uyğundursa, bura quş qoyun və şərhin yerinə \"Bu şərh yığışdırılıb\" yazısı çıxacaq."
+
+#: models.py:80
+msgid "comments"
+msgstr "şərhlər"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Bu şərh daxil olmuş istifadəçi adından yazılmışdır, buna görə də onun adını dəyişmək mümkün deyil."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Bu şərh daxil olmuş istifadəçi adından yazılmışdır, buna görə də onun e-poçt ünvanını dəyişmək mümkün deyil."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s %(date)s tarixində yazmışdır.\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "işarələ"
+
+#: models.py:180
+msgid "date"
+msgstr "tarix"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "şərh işarəsi"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "şərh işarələri"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Şərhə icazə ver"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Bu şərhi hamı görsün?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Təsdiqləyirəm"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Təsdiqlədiniz"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Saytımızda müzakirəni daha keyfiyyətli etmək üçün sərf etdiyiniz vaxta görə təşəkkür edirik."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Şərhi yığışdır"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Şərhi yığışdıraq?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Yığışdır"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Yığışdırdıq"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Şərhi işarələ"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Şərhi işarələyək?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "İşarələ"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "İşarələdik"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Yaz"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Baxım"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Şərh etdiyiniz üçün təşəkkür edirik"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Şərh etdiyiniz üçün təşəkkür edirik"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Şərhin görünüşü"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Şərhi göndər"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "və ya düzəliş et"
diff --git a/app/lib/django_comments/locale/be/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/be/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..770d1d7
--- /dev/null
+++ b/app/lib/django_comments/locale/be/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/be/LC_MESSAGES/django.po b/app/lib/django_comments/locale/be/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c825807
--- /dev/null
+++ b/app/lib/django_comments/locale/be/LC_MESSAGES/django.po
@@ -0,0 +1,302 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Belarusian (http://www.transifex.com/django/django-contrib-comments/language/be/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: be\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Зьмесьціва"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Зьвесткі"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "Пазначылі"
+msgstr[1] "пазначылі"
+msgstr[2] "Пазначылі"
+msgstr[3] "Пазначылі"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Пазначыць абраныя выказваньні"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "Ухвалілі"
+msgstr[1] "Ухвалілі"
+msgstr[2] "Ухвалілі"
+msgstr[3] "Ухвалілі"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Ухваліць абраныя выказваньні"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "Прыбралі"
+msgstr[1] "Прыбралі"
+msgstr[2] "Прыбралі"
+msgstr[3] "Прыбралі"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Прыбраць абраныя выказваньні"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(action)s %(count)s заўвагу."
+msgstr[1] "%(action)s %(count)s заўвагі."
+msgstr[2] "%(action)s %(count)s заўвагаў."
+msgstr[3] "%(action)s %(count)s заўвагаў."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Выказваньні на %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Найноўшыя выказваньні на %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Адрас эл. пошты"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Сеціўная спасылка"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Выказваньне"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Сачыце за сваімі словамі! Тут нельга казаць «%s»."
+msgstr[1] "Сачыце за сваімі словамі! Тут нельга казаць «%s»."
+msgstr[2] "Сачыце за сваімі словамі! Тут нельга казаць «%s»."
+msgstr[3] "Сачыце за сваімі словамі! Тут нельга казаць «%s»."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "і"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Калі напісаць нешта ў гэтым полі, выказваньне будзе лічыцца лухтою (спамам)."
+
+#: models.py:23
+msgid "content type"
+msgstr "від зьмесьціва"
+
+#: models.py:25
+msgid "object ID"
+msgstr "нумар аб’екта"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "карыстальнік"
+
+#: models.py:55
+msgid "user's name"
+msgstr "імя карыстальніка"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "эл. пошта карыстальніка"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "сеціўная спасылка карыстальніка"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "выкавзаньне"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "час і дата выказваньня"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Адрас IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "бачнае"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Прыбярыце гэтую птушачку, каб выказваньне зьнікла з пляцоўкі."
+
+#: models.py:67
+msgid "is removed"
+msgstr "прыбралі"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Абярыце, калі выказваньне не да месца або не адпавядае правілам. Замест яго будзе надпіс «Выказваньне прыбралі»."
+
+#: models.py:80
+msgid "comments"
+msgstr "выказваньні"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Выказваньне пакінуў карыстальнік, які апазнаўся, таму ягонае імя нельга зьмяняць."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Выказваньне пакінуў карыстальнік, які апазнаўся, таму ягоны адрас эл. пошты нельга зьмяняць."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(date)s, аўтар — %(user)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "пазнака"
+
+#: models.py:180
+msgid "date"
+msgstr "дата"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "пазнака выказваньня"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "пазнакі выказваньняў"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Ухваліць выказваньне"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Ці сапраўды зрабіць выказваньне бачным?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Ухваліць"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Дзякуем, што ўхвалілі"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Мы ўдзячныя, што вы дапамагаеце палепшыць якасьць размовы на нашай пляцоўцы"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Прыбраць выказваньне"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Ці сапраўды прыбраць выказваньне?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Прыбраць"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Дзякуем, што прыбралі"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Пазначыць выказваньне"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Ці сапраўды пазначыць выказваньне?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Пазначыць"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Дзякуем, што пазначылі"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Даслаць"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Прагледзець"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Дзякуем, што выказаліся"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Мы ўдзячныя за вашае выказваньне"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Прагледзець выказваньне"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Дашліце выказваньне"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "або выпраўце яго"
diff --git a/app/lib/django_comments/locale/bg/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/bg/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c7d1f3b
--- /dev/null
+++ b/app/lib/django_comments/locale/bg/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/bg/LC_MESSAGES/django.po b/app/lib/django_comments/locale/bg/LC_MESSAGES/django.po
new file mode 100644
index 0000000..71232ef
--- /dev/null
+++ b/app/lib/django_comments/locale/bg/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Boris Chervenkov <office@sentido.bg>, 2012
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Todor Lubenov <tgl.sysdev@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Bulgarian (http://www.transifex.com/django/django-contrib-comments/language/bg/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Благодаря за маркирането"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Метаданни"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "маркиран"
+msgstr[1] "маркирани"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Маркирай избраните коментари"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "одобрен"
+msgstr[1] "одобрени"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Одобри избраните коментари"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "отстранен"
+msgstr[1] "отстранени"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Премахване на избраните коментари"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s коментар беше успешно %(action)s."
+msgstr[1] "%(count)s коментари бяха успешно %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s коментари"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Последни коментари на %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Email адрес"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL адрес"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Коментар"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Внимание! Думата %s не се допуска."
+msgstr[1] "Внимание! Думите %s не се допускат."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "и"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ако въведете нещо в това поле, вашия коментар ще се третира като спам"
+
+#: models.py:23
+msgid "content type"
+msgstr "тип на съдържанието"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID на обекта"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "потребител"
+
+#: models.py:55
+msgid "user's name"
+msgstr "потребителско име"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "email адрес на потребителя"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL адрес на потребителя"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "коментар"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "дата и час на подаване"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP адрес"
+
+#: models.py:64
+msgid "is public"
+msgstr "е публичен"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Махнете отметката от това поле, за да премахнете коментара от сайта."
+
+#: models.py:67
+msgid "is removed"
+msgstr "е премахнат"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Щракнете тук ако коментарът е неподходящ. Вместо съдържанието на коментара, ще се покаже надписът \"Този коментар беше премахнат.\""
+
+#: models.py:80
+msgid "comments"
+msgstr "коментари"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Този коментар е публикуван от регистриран потребител, затова името не може да бъде редактирано."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Този коментар е публикуван от регистриран потребител, затова email адресът не може да бъде редактиран."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Публикуван от %(user)s на %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "маркиране"
+
+#: models.py:180
+msgid "date"
+msgstr "дата"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "отбелязване на коментар"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "отбелязване на коментари"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Одобряване на коментар"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Наистина ли да стане този коментар публичен?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Одобри"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Благодарим за одобрението"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Благодарим, че отделихте време, за да се подобри качеството на обсъждането на нашия сайт"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Премахване на коментар"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Сигурни ли сте, че искате да премахнете този коментар?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Премахване"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Благодарим за премахването"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Маркирай този коментар"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Сигурни ли сте, че искате да отбележете този коментар?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Отбелязване"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Благодарим за отбелязването"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Публикувай"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Преглед"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Благодарим за коментара"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Благодарим за Вашия коментар"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Преглед на коментар"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Публикувай коментар"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "или направете промени"
diff --git a/app/lib/django_comments/locale/bn/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/bn/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..2343fc4
--- /dev/null
+++ b/app/lib/django_comments/locale/bn/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/bn/LC_MESSAGES/django.po b/app/lib/django_comments/locale/bn/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a60c49f
--- /dev/null
+++ b/app/lib/django_comments/locale/bn/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Anubhab Baksi, 2013
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Bengali (http://www.transifex.com/django/django-contrib-comments/language/bn/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bn\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "কনটেন্ট"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "মেটাডাটা"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "ফ্ল্যাগ করা হয়েছে"
+msgstr[1] "ফ্ল্যাগ করা হয়েছে"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "চয়িত মন্তব্যগুলোকে ফ্ল্যাগ করুন"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "অনুমোদিত"
+msgstr[1] "অনুমোদিত"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "চিহ্নিত মন্তব্যগুলি অনুমোদন করুন"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "মুছে ফেলা হয়েছে"
+msgstr[1] "মুছে ফেলা হয়েছে"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "চয়িত মন্তব্যগুলি মুছে ফেলুন"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ইমেইল ঠিকানা"
+
+#: forms.py:98
+msgid "URL"
+msgstr "ইউআরএল (URL)"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "মন্তব্য"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "সাবধান! %s শব্দটি এখানে প্রযোজ্য নয়।"
+msgstr[1] "সাবধান! %s শব্দগুলো এখানে প্রযোজ্য নয়।"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "এবং"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "আপনি যদি এখানে কোনকিছু লিখেন তবে আপনার মন্তব্যকে স্প্যাম হিসেবে ধরা হবে"
+
+#: models.py:23
+msgid "content type"
+msgstr "কনটেন্ট টাইপ"
+
+#: models.py:25
+msgid "object ID"
+msgstr "অবজেক্ট আইডি"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "সদস্য"
+
+#: models.py:55
+msgid "user's name"
+msgstr "সদস্যের নাম"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "সদস্যের ইমেইল ঠিকানা"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "সদস্যের ইউআরএল (URL)"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "মন্তব্য"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "দাখিলের তারিখ/সময়"
+
+#: models.py:63
+msgid "IP address"
+msgstr "আইপি ঠিকানা"
+
+#: models.py:64
+msgid "is public"
+msgstr "সার্বজনীন"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "সাইট থেকে মন্তব্য মুছে ফেলতে এখানে আনচেক করুন।"
+
+#: models.py:67
+msgid "is removed"
+msgstr "মোছা হয়েছে"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "এই বাক্সে চেক করুন যদি মন্তব্যটি যথাযথ না হয়। মন্তব্যের পরিবর্তে \"মন্তব্যদি মুছে ফেলা হয়েছে\" দেখানো হবে।"
+
+#: models.py:80
+msgid "comments"
+msgstr ""
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "এই মন্তব্যটি একজন নিবন্ধনকৃত সদস্য করেছেন, সেজন্যই নামটি শুধুমাত্র পড়ার যোগ্য।"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "এই মন্তব্যটি একজন নিবন্ধনকৃত সদস্য করেছেন, সেজন্যই ইমেইল ঠিকানা শুধুমাত্র পড়ার যোগ্য।"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "লিখেছেন %(user)s - %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "পতাকা"
+
+#: models.py:180
+msgid "date"
+msgstr "তারিখ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "মন্তব্য পতাকা"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "মন্তব্য পতাকাসমূহ"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "একটি মন্তব্য অনুমোদন করুন"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "সত্যিই কি এই মন্তব্যকে সাধারণের জন্য উন্মুক্ত করতে চান?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "অনুমোদন"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "অনুমোদন করার জন্য ধন্যবাদ"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "আমাদের সাইটের উন্নতিকল্পে আলোচনায় যোগ দেওয়ার জন্য আপনাকে সাধুবাদ জানাই"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "একটি মন্তব্য মুছে ফেলুন"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "সত্যিই কি এই মন্তব্যকে উড়িয়ে দিতে চান?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "মুছে ফেলুন"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "মুছে ফেলার জন্য ধন্যবাদ"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "এই মন্তব্যকে পতাকাচিহ্নিত করুন"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "সত্যিই কি এই মন্তব্যকে পতাকাচিহ্নিত করতে চান?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "পতাকা"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "পতাকাচিহ্নিত করার জন্য ধন্যবাদ"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "প্রাকদর্শন"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "মন্তব্য লেখার জন্য ধন্যবাদ"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "আপনার মতামতের জন্য ধন্যবাদ"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "আপনার মন্তব্যকে প্রাকদর্শন করুন"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/br/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/br/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6bc591c
--- /dev/null
+++ b/app/lib/django_comments/locale/br/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/br/LC_MESSAGES/django.po b/app/lib/django_comments/locale/br/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7ef12b5
--- /dev/null
+++ b/app/lib/django_comments/locale/br/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Fulup <fulup.jakez@gmail.com>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Breton (http://www.transifex.com/django/django-contrib-comments/language/br/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: br\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Danvez"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metaroadennoù"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "merket"
+msgstr[1] "merket"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Merkañ an evezhiadennoù diuzet"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprouet"
+msgstr[1] "aprouet"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprouiñ an evezhiadennoù diuzet"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "dilamet"
+msgstr[1] "dilamet"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Dilemel an evezhiadennoù diuzet"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Chomlec'h postel"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Evezhiadenn"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr ""
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "seurt danvez"
+
+#: models.py:25
+msgid "object ID"
+msgstr ""
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "implijer"
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr "chomlec'h postel an implijer"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL an implijer"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "danvez"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "deiziad/eur kaset"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Chomlec'h IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "zo foran"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "zo bet dilamet"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr "evezhiadennoù"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr "merker"
+
+#: models.py:180
+msgid "date"
+msgstr "deiziad"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "aprouiñ un evezhiadenn"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprouiñ"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Trugarez da vezañ aprouet"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Diverkañ un evezhiadenn"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Dilemel"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Trugarez evit an diverkadenn-mañ"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Kas"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Rakwelet"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Rakwelet hoc'h evezhiadenn"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/bs/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/bs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6707dad
--- /dev/null
+++ b/app/lib/django_comments/locale/bs/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/bs/LC_MESSAGES/django.po b/app/lib/django_comments/locale/bs/LC_MESSAGES/django.po
new file mode 100644
index 0000000..13f3fd1
--- /dev/null
+++ b/app/lib/django_comments/locale/bs/LC_MESSAGES/django.po
@@ -0,0 +1,298 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Filip Dupanović <filip.dupanovic@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Bosnian (http://www.transifex.com/django/django-contrib-comments/language/bs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bs\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Sadržaj"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metapodaci"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "označen"
+msgstr[1] "označena"
+msgstr[2] "označena"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označite izabrane komentare"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "odobren"
+msgstr[1] "odobrena"
+msgstr[2] "odobrena"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Odobri izabrane komentare"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "izbrisan"
+msgstr[1] "izbrisana"
+msgstr[2] "izbrisan"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Obriši izabrane komentare"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentar je uspješno %(action)s."
+msgstr[1] "%(count)s komentara su uspješno %(action)s."
+msgstr[2] "%(count)s komentara su uspješno %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Komentari na %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Najnoviji komentari na %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Email adresa"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pazite šta pišete! Riječ %s nije dozvoljena ovdje."
+msgstr[1] "Pazite šta pišete! Riječi %s nisu dozvoljene ovdje."
+msgstr[2] "Pazite šta pišete! Riječi %s nisu dozvoljene ovdje."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "i"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ako unesete bilo šta u ovo polje, Vaš komentar će se smatrati spamom"
+
+#: models.py:23
+msgid "content type"
+msgstr "tip sadržaja"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID objekta"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "korisnik"
+
+#: models.py:55
+msgid "user's name"
+msgstr "korisnikovo ime"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "korisnikova email adresa"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "korisnikov URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum/vrijeme unosa"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresa"
+
+#: models.py:64
+msgid "is public"
+msgstr "javno dostupan"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Uklonite izbor ovog polja da bi se komentar izbrisao sa stranice."
+
+#: models.py:67
+msgid "is removed"
+msgstr "uklonjen"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Obilježite ovo polje ukoliko je komentar neprikladan. Prikazat će se poruka \"Komentar je ukonjen\" umjesto komentara."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentari"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ovaj komentar je postavio prijavljeni korisnik i ime se ne može mijenjati."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ovaj komentar je postavio prijavljeni korisnik i email se ne može mijenjati."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postavio %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "oznaka"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "oznaka komentara"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "oznake komentara"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Odobri komentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Da li zaista želite da učinite ovaj komentar javno dostupnim?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Odobri"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Hvala na odobrenju"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Hvala što ste izdvojili vrijeme da poboljšate kvalitet diskusije na našoj stranici"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Obriši komentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Da li zaista želite da obrišete ovaj komentar?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Obriši"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Hvala na brisanju"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označi ovaj komentar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Da li zaista želite da označite ovaj komentar?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Označi"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Hvala što ste označili komentar."
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Postavi"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Pregled"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Hvala na komentaru"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Hvala što ste ostavili svoj komentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Pregledaj komentar"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Postavi komentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ili izvrši izmjene"
diff --git a/app/lib/django_comments/locale/ca/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ca/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..be40482
--- /dev/null
+++ b/app/lib/django_comments/locale/ca/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ca/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ca/LC_MESSAGES/django.po
new file mode 100644
index 0000000..81c552d
--- /dev/null
+++ b/app/lib/django_comments/locale/ca/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Catalan (http://www.transifex.com/django/django-contrib-comments/language/ca/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contingut"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadades"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcat"
+msgstr[1] "marcats"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar els comentaris seleccionats"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprovat"
+msgstr[1] "aprovats"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprovar els comentaris seleccionats"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminat"
+msgstr[1] "eliminats"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eliminar els comentaris seleccionats"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 comentari ha estat %(action)s satisfactòriament."
+msgstr[1] "%(count)s comentaris han estat %(action)s satisfactòriament."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "comentaris de %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últims comentaris a %(site_name)s."
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adreça de correu electrònic"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentari"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Vigileu amb el vostre llenguatge! Aquí no s'admet la paraula: %s."
+msgstr[1] "Vigileu amb el vostre llenguatge! Aquí no s'admeten les paraules: %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "i"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si entreu qualsevol cosa en aquest camp el vostre comentari es tractarà com a spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipus de contingut"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID de l'objecte"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuari"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nom de l'usuari"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adreça de correu electrònic de l'usuari"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL de l'usuari"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentari"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hora d'enviament"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Adreça IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "és públic"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Desmarqueu aquesta casella per fer desaparèixer aquest comentari del lloc web de forma efectiva."
+
+#: models.py:67
+msgid "is removed"
+msgstr "està eliminat"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marqueu aquesta casella si el comentari no és apropiat. En lloc seu es mostrarà \"Aquest comentari ha estat eliminat\" "
+
+#: models.py:80
+msgid "comments"
+msgstr "comentaris"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Aquest comentari va ser publicat per un usuari autentificat, per això el seu nom no es pot modificar."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Aquest comentari va ser publicat per un usuari autentificat, per això la seva adreça de correu electrònic no es pot modificar."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Enviat per %(user)s el %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marcar"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marca del comentari"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marques del comentari"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprovar un comentari"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Voleu realment fer públic aquest comentari?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprovar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Gràcies per aprovar"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Gràcies per dedicar el temps a millorar la qualitat del debat al nostre lloc"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eliminar un comentari"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Realment voleu eliminar aquest comentari?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Gràcies per eliminar"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar aquest comentari"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Realment voleu marcar aquest comentari?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Gràcies per marcar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publicar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Vista prèvia"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Gràcies per comentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Gràcies pel vostre comentari"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Previsualitzar el vostre comentari"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Enviar el seu comentari"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o feu canvis."
diff --git a/app/lib/django_comments/locale/cs/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..3734dcc
--- /dev/null
+++ b/app/lib/django_comments/locale/cs/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/cs/LC_MESSAGES/django.po b/app/lib/django_comments/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4ff7fdd
--- /dev/null
+++ b/app/lib/django_comments/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,303 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Vláďa Macek <macek@sandbox.cz>, 2015-2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-07-24 18:06+0000\n"
+"Last-Translator: Vláďa Macek <macek@sandbox.cz>\n"
+"Language-Team: Czech (http://www.transifex.com/django/django-contrib-comments/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Obsah"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "označen"
+msgstr[1] "označeny"
+msgstr[2] "označeno"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označit vybrané komentáře"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "schválen"
+msgstr[1] "schváleny"
+msgstr[2] "schváleno"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Schválit vybrané komentáře"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "odebrán"
+msgstr[1] "odebrány"
+msgstr[2] "odebráno"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Odebrat vybrané komentáře"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 komentář byl úspěšně %(action)s."
+msgstr[1] "%(count)s komentáře byly úspěšně %(action)s."
+msgstr[2] "%(count)s komentářů bylo úspěšně %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Komentáře z webu %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Poslední komentáře na webu %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Jméno"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mailová adresa"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentář"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Mluvte slušně! Slovo %s je zde nepřípustné."
+msgstr[1] "Mluvte slušně! Slova %s jsou zde nepřípustná."
+msgstr[2] "Mluvte slušně! Slova %s jsou zde nepřípustná."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "a"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Jestliže do tohoto pole cokoli zadáte, bude komentář považován za spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "typ obsahu"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID položky"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "uživatel"
+
+#: models.py:55
+msgid "user's name"
+msgstr "jméno uživatele"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "e-mailová adresa uživatele"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL uživatele"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentář"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum a čas byly zaslané"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Adresa IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "je veřejný"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Pokud zrušíte zaškrtnutí tohoto políčka, komentář se na stránce nezobrazí."
+
+#: models.py:67
+msgid "is removed"
+msgstr "je odebrán"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Zaškrtněte, pokud je komentář nevhodný. Místo něj bude zobrazena zpráva \"Tento komentář byl odebrán\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentář"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Tento komentář zaslal přihlášený uživatel, jméno tedy není možné změnit."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Tento komentář zaslal přihlášený uživatel, e-mail tedy není možné změnit."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Zadal uživatel %(user)s dne %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "značka"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "značka komentáře"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "značky komentáře"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] K položce \"%(object)s\" byl přidán komentář"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Schválit komentář"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Opravdu chcete zveřejnit tento komentář?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Schválit"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Děkujeme za schválení"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Děkujeme za váš čas věnovaný zlepšení kvality diskuze na našich stránkách"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Odebrat komentář"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Opravdu chcete odebrat tento komentář?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Odebrat"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Děkujeme za odebrání"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označit tento komentář"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Opravdu chcete označit tento komentář?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Označit"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Děkujeme za označení komentáře"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Odeslat"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Náhled"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Děkujeme za vložení komentáře"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Děkujeme za komentář"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Zobrazit náhled komentáře"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Opravte níže uvedenou chybu"
+msgstr[1] "Opravte níže uvedené chyby"
+msgstr[2] "Opravte níže uvedené chyby"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Komentář odeslat"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "nebo upravit"
diff --git a/app/lib/django_comments/locale/cy/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/cy/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..459cca9
--- /dev/null
+++ b/app/lib/django_comments/locale/cy/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/cy/LC_MESSAGES/django.po b/app/lib/django_comments/locale/cy/LC_MESSAGES/django.po
new file mode 100644
index 0000000..73b0d95
--- /dev/null
+++ b/app/lib/django_comments/locale/cy/LC_MESSAGES/django.po
@@ -0,0 +1,303 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Welsh (http://www.transifex.com/django/django-contrib-comments/language/cy/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cy\n"
+"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr ""
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Sylw"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ac"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "math cynnwys"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID gwrthrych"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr ""
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "sylw"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dyddiad/amser wedi ymostwng"
+
+#: models.py:63
+msgid "IP address"
+msgstr "cyfeiriad IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "yn gyhoeddus"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "wedi diddymu"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr ""
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postiwyd gan %(user)s ar %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr ""
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/da/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/da/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a755fa1
--- /dev/null
+++ b/app/lib/django_comments/locale/da/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/da/LC_MESSAGES/django.po b/app/lib/django_comments/locale/da/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ec3ee93
--- /dev/null
+++ b/app/lib/django_comments/locale/da/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Danish (http://www.transifex.com/django/django-contrib-comments/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Indhold"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "Markeret"
+msgstr[1] "Markeret"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marker valgte kommentarer"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "Godkendt"
+msgstr[1] "Godkendt"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Godkend valgte kommentarer"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "fjernet"
+msgstr[1] "fjernet"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Fjern valgte kommentarer"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 kommentar blev %(action)s"
+msgstr[1] "%(count)s kommentarer blev %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "kommentarer på %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Seneste kommentarer på %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mail-adresse"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Var din mund! Ordet %s er ikke tilladt her."
+msgstr[1] "Var din mund! Ordene %s er ikke tilladt her."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "og"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Hvis du indtaster noget i dette felt, vil din kommentar blive betragtet som spam."
+
+#: models.py:23
+msgid "content type"
+msgstr "indholdstype"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekt-ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "bruger"
+
+#: models.py:55
+msgid "user's name"
+msgstr "brugerens navn"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "brugerens e-mail-adresse"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "brugerens URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dato/tidspunkt for oprettelse"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adresse"
+
+#: models.py:64
+msgid "is public"
+msgstr "er offentlig"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Hvis du fjerner afkrydsningen her, bliver din kommentar slettet fra sitet."
+
+#: models.py:67
+msgid "is removed"
+msgstr "er fjernet"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Afkryds denne boks, hvis kommentaren er upassende. Beskeden \"Denne kommentar er blevet fjernet\" vil blive vist i stedet."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentarer"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Denne kommentar blev indsendt af en autenticeret bruger; derfor er navnet skrivebeskyttet."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Denne kommentar blev indsendt af en autenticeret bruger; derfor er e-mail-adressen skrivebeskyttet."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Indsendt af %(user)s den %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "Flag"
+
+#: models.py:180
+msgid "date"
+msgstr "dato"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentarflag"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommentarflag"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Godkend en kommentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Vil du godkende denne kommentar?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Godkend"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Tak for godkendelsen"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Tak fordi du tog dig tid til at højne kvaliteten af diskussionen på vores website"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Fjern en kommentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Skal kommentaren fjernes?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Fjern"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Tak for fjernelsen"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flag denne kommentar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Skal kommentaren flages?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flag"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Tak for markeringen"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Indsend"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Forhåndsvis"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Tak for kommenteringen"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Tak for kommentaren"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Forhåndsvis kommentar"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Indsend din kommentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "eller gennemfør ændringer"
diff --git a/app/lib/django_comments/locale/de/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c705a54
--- /dev/null
+++ b/app/lib/django_comments/locale/de/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/de/LC_MESSAGES/django.po b/app/lib/django_comments/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..802f837
--- /dev/null
+++ b/app/lib/django_comments/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: German (http://www.transifex.com/django/django-contrib-comments/language/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Inhalt"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadaten"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "markiert"
+msgstr[1] "markiert"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Ausgewählte Kommentare markieren"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "freigegeben"
+msgstr[1] "freigegeben"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Ausgewählte Kommentare freigeben"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "entfernt"
+msgstr[1] "entfernt"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Ausgewählte Kommentare entfernen"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 Kommentar wurde erfolgreich %(action)s."
+msgstr[1] "%(count)s Kommentare wurden erfolgreich %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s-Kommentare"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Die neuesten Kommentare auf %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-Mail-Adresse"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Adresse (URL)"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Keine Schimpfworte! Das Wort %s ist hier nicht erlaubt!"
+msgstr[1] "Keine Schimpfworte! Die Wörter %s sind hier nicht erlaubt!"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "und"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Wenn Sie irgendetwas in dieses Feld eintragen, wird der Kommentar als Spam betrachtet"
+
+#: models.py:23
+msgid "content type"
+msgstr "Inhaltstyp"
+
+#: models.py:25
+msgid "object ID"
+msgstr "Objekt-ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "Benutzer"
+
+#: models.py:55
+msgid "user's name"
+msgstr "Benutzername"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "E-Mail-Adresse"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "Kommentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "Datum/Zeit Erstellung"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-Adresse"
+
+#: models.py:64
+msgid "is public"
+msgstr "ist öffentlich"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Deaktivieren, um den Kommentar sofort von der Website zu entfernen."
+
+#: models.py:67
+msgid "is removed"
+msgstr "ist gelöscht"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Hier einen Haken setzen, wenn der Kommentar unpassend ist. Stattdessen wird dann \"Dieser Kommentar wurde entfernt\" Meldung angezeigt."
+
+#: models.py:80
+msgid "comments"
+msgstr "Kommentare"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Dieser Kommentar wurde von einem authentifizierten Benutzer geschrieben.\nDer Name ist daher schreibgeschützt.\n\n%(text)s"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Dieser Kommentar wurde von einem authentifizierten Benutzer geschrieben.\nDie E-Mail-Adresse ist daher schreibgeschützt.\n\n%(text)s"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Geschrieben von %(user)s am %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "Markierung"
+
+#: models.py:180
+msgid "date"
+msgstr "Datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "Kommentar-Markierung"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "Kommentar-Markierungen"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Kommentar freigeben"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Wollen Sie diesen Kommentar wirklich freigeben?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Freigeben"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Vielen Dank, dass Sie den Kommentar freigegeben haben"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Vielen Dank, dass Sie dabei mithelfen, die Qualität der Diskussion auf unserer Website zu verbessern"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Kommentar entfernen"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Wollen Sie diesen Kommentar wirklich entfernen?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Entfernen"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Vielen Dank, dass Sie diesen Kommentar entfernt haben"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Diesen Kommentar markieren"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Wollen Sie diesen Kommentar wirklich markieren?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Markierung"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Vielen Dank, dass Sie diesen Kommentar markiert haben"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Abschicken"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Vorschau"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Vielen Dank, dass Sie einen Kommentar geschrieben haben"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Vielen Dank für Ihren Kommentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Kommentarvorschau"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Kommentar abschicken"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "oder Änderungen vornehmen"
diff --git a/app/lib/django_comments/locale/el/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/el/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..84ce13e
--- /dev/null
+++ b/app/lib/django_comments/locale/el/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/el/LC_MESSAGES/django.po b/app/lib/django_comments/locale/el/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e5cd20c
--- /dev/null
+++ b/app/lib/django_comments/locale/el/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Dimitris Glezos <glezos@indifex.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Nick Mavrakis <mavrakis.n@gmail.com>, 2016
+# Yorgos Pagles <y.pagles@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-07-15 13:02+0000\n"
+"Last-Translator: Nick Mavrakis <mavrakis.n@gmail.com>\n"
+"Language-Team: Greek (http://www.transifex.com/django/django-contrib-comments/language/el/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: el\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Περιεχόμενο"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Μεταδεδομένα"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "επισημασμένο"
+msgstr[1] "επισημασμένα"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Επισημανση των επιλεγμένων σχολίων"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "εγκρίθηκε"
+msgstr[1] "εγκρίθηκαν"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Έγκριση των συγκεκριμένων σχολίων"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "αφαιρέθηκε"
+msgstr[1] "αφαιρέθηκαν"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Αφαίρεση των επιλεγμένων σχολίων"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "Πραγματοποιήθκε επιτυχημένα %(action)s στα %(count)s σχόλια."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Σχόλια στο %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Τελευταία σχόλια στο %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Όνομα"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Ηλεκτρονική διεύθυνση"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Σχόλιο"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Η λέξη %s δεν επιτρέπεται σε σχόλια."
+msgstr[1] "Η λέξεις %s δεν επιτρέπονται σε σχόλια."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "και"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Αφήστε αυτό το πεδίο κενό. Αν εισάγετε κάτι τότε το σχόλιο θα θεωρηθεί spam και δεν θα εμφανιστεί."
+
+#: models.py:23
+msgid "content type"
+msgstr "τύπος περιεχομένου"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID αντικειμένου"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "χρήστης"
+
+#: models.py:55
+msgid "user's name"
+msgstr "όνομα χρήστη"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "ηλεκτρονική διεύθυνση χρήστη"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "διεύθυνση ιστοτόπου χρήστη"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "σχόλιο"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "ημερομηνία/ώρα υποβολής"
+
+#: models.py:63
+msgid "IP address"
+msgstr "διεύθυνση IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "είναι δημόσιο"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Απεπιλέξτε αυτή την επιλογή για να κάνετε το σχόλιο να μην εμφανίζεται."
+
+#: models.py:67
+msgid "is removed"
+msgstr "είναι διαγραμμένο"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Επιλέξτε αυτή την επιλογή εάν το σχόλιο είναι ανάρμοστο. Ένα μήνυμα \"Αυτό το σχόλιο διαγράφηκε\" θα εμφανιστεί στη θέση του."
+
+#: models.py:80
+msgid "comments"
+msgstr "σχόλια"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Αυτό το σχόλιο πραγματοποιήθκε από πιστοποιημένο χρήστη και για τον λόγο αυτό δεν είναι είναι δυνατή η επεξεργασία του ονόματός του."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Αυτό το σχόλιο πραγματοποιήθκε από πιστοποιημένο χρήστη και για τον λόγο αυτό δεν είναι είναι δυνατή η επεξεργασία της διεύθυνσης του ηλεκτρονικού του ταχυδρομείου."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Σχόλιο από %(user)s στις %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "επισήμανση"
+
+#: models.py:180
+msgid "date"
+msgstr "ημερομηνία"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "επισήμανση σχολίου"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "επισημάνσεις σχολίου"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Νέο σχόλιο δημοσιεύτηκε στο \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Έγκριση σχολίου"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Επιβεβαιώστε ότι επιθυμείτε την δημόσια εμφάνιση του σχολίου."
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Έγκριση."
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Ευχαριστούμε για την έγκριση."
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Ευχαριστούμε για τον χρόνο που διαθέσατε για την βελτίωση της ποιότητας των σχολίων στον ιστότοπό μας."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Αφαίρεση σχολίου"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Επιβεβαιώστε ότι επιθυμείτε την αφαίρεση του σχολίου."
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Αφαίρεση"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Ευχαριστούμε για την αφαίρεση"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Επισήμανση σχολίου"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Επιβεβαιώστε ότι επιθυμείτε την επισήμανσση του σχολίου."
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Επισήμανση"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Ευχαριστούμε για την επισήμανση"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Δημοσίευση"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Προβολή:"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Ευχαριστούμε για το σχόλιό σας"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Ευχαριστούμε για το σχόλιό σας"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Προβολή του σχολίου σας"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Παρακαλούμε διορθώστε τα παρακάτω σφάλματα:"
+msgstr[1] "Παρακαλούμε διορθώστε τα παρακάτω σφάλματα:"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Δημοσιοποίηση του σχολίου σας"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ή πραγματοποιήστε αλλαγές"
diff --git a/app/lib/django_comments/locale/en/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c810b1b
--- /dev/null
+++ b/app/lib/django_comments/locale/en/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/en/LC_MESSAGES/django.po b/app/lib/django_comments/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..2ff4d0f
--- /dev/null
+++ b/app/lib/django_comments/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,321 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011.
+msgid ""
+msgstr ""
+"Project-Id-Version: Django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-09-30 19:54+0200\n"
+"PO-Revision-Date: 2012-02-14 13:24+0000\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: English <en@li.org>\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: django_comments/abstracts.py:26
+msgid "content type"
+msgstr "content type"
+
+#: django_comments/abstracts.py:29
+msgid "object ID"
+msgstr "object ID"
+
+#: django_comments/abstracts.py:57 django_comments/models.py:32
+msgid "user"
+msgstr "user"
+
+#: django_comments/abstracts.py:60
+msgid "user's name"
+msgstr "user's name"
+
+#: django_comments/abstracts.py:62
+msgid "user's email address"
+msgstr "user's email address"
+
+#: django_comments/abstracts.py:64
+msgid "user's URL"
+msgstr "user's URL"
+
+#. Translators: 'comment' is a noun here.
+#: django_comments/abstracts.py:66 django_comments/abstracts.py:86
+#: django_comments/models.py:37
+msgid "comment"
+msgstr "comment"
+
+#: django_comments/abstracts.py:69
+msgid "date/time submitted"
+msgstr "date/time submitted"
+
+#: django_comments/abstracts.py:70
+msgid "IP address"
+msgstr "IP address"
+
+#: django_comments/abstracts.py:71
+msgid "is public"
+msgstr "is public"
+
+#: django_comments/abstracts.py:72
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+"Uncheck this box to make the comment effectively disappear from the site."
+
+#: django_comments/abstracts.py:74
+msgid "is removed"
+msgstr "is removed"
+
+#: django_comments/abstracts.py:75
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+
+#: django_comments/abstracts.py:87
+msgid "comments"
+msgstr "comments"
+
+#: django_comments/abstracts.py:132
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+
+#: django_comments/abstracts.py:143
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+
+#: django_comments/abstracts.py:171
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+
+#: django_comments/admin.py:28
+msgid "Content"
+msgstr "Content"
+
+#: django_comments/admin.py:32
+msgid "Metadata"
+msgstr "Metadata"
+
+#: django_comments/admin.py:59
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flagged"
+msgstr[1] "flagged"
+
+#: django_comments/admin.py:61
+msgid "Flag selected comments"
+msgstr "Flag selected comments"
+
+#: django_comments/admin.py:65
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "approved"
+msgstr[1] "approved"
+
+#: django_comments/admin.py:67
+msgid "Approve selected comments"
+msgstr "Approve selected comments"
+
+#: django_comments/admin.py:71
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "removed"
+msgstr[1] "removed"
+
+#: django_comments/admin.py:73
+msgid "Remove selected comments"
+msgstr "Remove selected comments"
+
+#: django_comments/admin.py:85
+#, fuzzy, python-format
+#| msgid "1 comment was successfully %(action)s."
+#| msgid_plural "%(count)s comments were successfully %(action)s."
+msgid "%(count)s comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 comment was successfully %(action)s."
+msgstr[1] "%(count)s comments were successfully %(action)s."
+
+#: django_comments/feeds.py:16
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s comments"
+
+#: django_comments/feeds.py:22
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Latest comments on %(site_name)s"
+
+#: django_comments/forms.py:101
+msgctxt "Person name"
+msgid "Name"
+msgstr "Name"
+
+#: django_comments/forms.py:102
+msgid "Email address"
+msgstr "Email address"
+
+#: django_comments/forms.py:103
+msgid "URL"
+msgstr "URL"
+
+#. Translators: 'Comment' is a noun here.
+#: django_comments/forms.py:105
+msgid "Comment"
+msgstr "Comment"
+
+#: django_comments/forms.py:183
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Watch your mouth! The word %s is not allowed here."
+msgstr[1] "Watch your mouth! The words %s are not allowed here."
+
+#: django_comments/forms.py:187
+#: django_comments/templates/comments/preview.html:17
+msgid "and"
+msgstr "and"
+
+#: django_comments/forms.py:193
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+"If you enter anything in this field your comment will be treated as spam"
+
+#. Translators: 'flag' is a noun here.
+#: django_comments/models.py:40
+msgid "flag"
+msgstr "flag"
+
+#: django_comments/models.py:39
+msgid "date"
+msgstr "date"
+
+#: django_comments/models.py:49
+msgid "comment flag"
+msgstr "comment flag"
+
+#: django_comments/models.py:50
+msgid "comment flags"
+msgstr "comment flags"
+
+#: django_comments/moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr ""
+
+#: django_comments/templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Approve a comment"
+
+#: django_comments/templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Really make this comment public?"
+
+#: django_comments/templates/comments/approve.html:13
+msgid "Approve"
+msgstr "Approve"
+
+#: django_comments/templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Thanks for approving"
+
+#: django_comments/templates/comments/approved.html:7
+#: django_comments/templates/comments/deleted.html:7
+#: django_comments/templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+
+#: django_comments/templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Remove a comment"
+
+#: django_comments/templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Really remove this comment?"
+
+#: django_comments/templates/comments/delete.html:13
+msgid "Remove"
+msgstr "Remove"
+
+#: django_comments/templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Thanks for removing"
+
+#: django_comments/templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flag this comment"
+
+#: django_comments/templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Really flag this comment?"
+
+#: django_comments/templates/comments/flag.html:13
+msgid "Flag"
+msgstr "Flag"
+
+#: django_comments/templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Thanks for flagging"
+
+#: django_comments/templates/comments/form.html:18
+#: django_comments/templates/comments/preview.html:34
+msgid "Post"
+msgstr "Post"
+
+#: django_comments/templates/comments/form.html:19
+#: django_comments/templates/comments/preview.html:35
+msgid "Preview"
+msgstr "Preview"
+
+#: django_comments/templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Thanks for commenting"
+
+#: django_comments/templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Thank you for your comment"
+
+#: django_comments/templates/comments/preview.html:4
+#: django_comments/templates/comments/preview.html:14
+msgid "Preview your comment"
+msgstr "Preview your comment"
+
+#: django_comments/templates/comments/preview.html:12
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Please correct the error below"
+msgstr[1] "Please correct the errors below"
+
+#: django_comments/templates/comments/preview.html:17
+msgid "Post your comment"
+msgstr "Post your comment"
+
+#. Translators: This string follows the 'Post your comment' submit button.
+#: django_comments/templates/comments/preview.html:20
+msgid "or make changes"
+msgstr "or make changes"
diff --git a/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..d2de857
--- /dev/null
+++ b/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.po b/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.po
new file mode 100644
index 0000000..f75d220
--- /dev/null
+++ b/app/lib/django_comments/locale/en_GB/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Ross Poulton <ross@rossp.org>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: English (United Kingdom) (http://www.transifex.com/django/django-contrib-comments/language/en_GB/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en_GB\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Content"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flagged"
+msgstr[1] "flagged"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Flag selected comments"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "approved"
+msgstr[1] "approved"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Approve selected comments"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "removed"
+msgstr[1] "removed"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Remove selected comments"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "%(count)s comments were successfully %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s comments"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Latest comments on %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Email address"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comment"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Watch your mouth! The word %s is not allowed here."
+msgstr[1] "Watch your mouth! The words %s are not allowed here."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "and"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "If you enter anything in this field your comment will be treated as spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "content type"
+
+#: models.py:25
+msgid "object ID"
+msgstr "object ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "user"
+
+#: models.py:55
+msgid "user's name"
+msgstr "user's name"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "user's email address"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "user's URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comment"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "date/time submitted"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP address"
+
+#: models.py:64
+msgid "is public"
+msgstr "is public"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Uncheck this box to make the comment effectively disappear from the site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "is removed"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Check this box if the comment is inappropriate. A \"This comment has been removed\" message will be displayed instead."
+
+#: models.py:80
+msgid "comments"
+msgstr "comments"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "This comment was posted by an authenticated user and thus the name is read-only."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "This comment was posted by an authenticated user and thus the email is read-only."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flag"
+
+#: models.py:180
+msgid "date"
+msgstr "date"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "comment flag"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "comment flags"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Approve a comment"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Really make this comment public?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Approve"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Thanks for approving"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Thanks for taking the time to improve the quality of discussion on our site"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Remove a comment"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Really remove this comment?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Remove"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Thanks for removing"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flag this comment"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Really flag this comment?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flag"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Thanks for flagging"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Post"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Preview"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Thanks for commenting"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Thank you for your comment"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Preview your comment"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Post your comment"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "or make changes"
diff --git a/app/lib/django_comments/locale/eo/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/eo/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..bb2a28f
--- /dev/null
+++ b/app/lib/django_comments/locale/eo/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/eo/LC_MESSAGES/django.po b/app/lib/django_comments/locale/eo/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c03e004
--- /dev/null
+++ b/app/lib/django_comments/locale/eo/LC_MESSAGES/django.po
@@ -0,0 +1,296 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Nikolay Korotkiy <sikmir@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-11-11 12:09+0000\n"
+"Last-Translator: Nikolay Korotkiy <sikmir@gmail.com>\n"
+"Language-Team: Esperanto (http://www.transifex.com/django/django-contrib-comments/language/eo/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: eo\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Enhavo"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadatumo"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "markita"
+msgstr[1] "markitaj"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marki elektitajn komentojn"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobita"
+msgstr[1] "aprobitaj"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprobi elektitajn komentojn"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "forigita"
+msgstr[1] "forigitaj"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Forigi elektitajn komentojn"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 komento estis suksese %(action)s."
+msgstr[1] "%(count)s komentoj estis suksese %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s komentoj"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Lastaj komentoj ĉe %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nomo"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Retpoŝtadreso"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komento"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Atentu via lingvaĵo! La vorto %s ne estas permisita ĉi-tie."
+msgstr[1] "Atentu via lingvaĵo! La vortoj %s ne estas permisitaj ĉi-tie."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "kaj"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Se vi enigas ion-ajn en ĉi-tiu kampo, via komento estos traktita kiel spamo"
+
+#: models.py:23
+msgid "content type"
+msgstr "enhava tipo"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekta identigaĵo"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "uzanto"
+
+#: models.py:55
+msgid "user's name"
+msgstr "uzanta nomo"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "uzanta retpoŝtadreso"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "uzanta URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komento"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dato kaj horo transsenditaj"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adreso"
+
+#: models.py:64
+msgid "is public"
+msgstr "estas publika"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Malŝaltu ĉi-tiun markobutonon por definitive malaperigi la komenton el la retejo."
+
+#: models.py:67
+msgid "is removed"
+msgstr "estas forigita"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Ŝaltu ĉi-tiun markobutonon se la komento estas nekonvena. La mesaĝo \"Ĉi-tiu komento estis forigita\" estos montrita anstataŭe."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentoj"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ĉi-tiu komento estis afiŝita de aŭtentigita uzanto, do tiel la nomo estas nurlega."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ĉi-tiu komento estis afiŝita de aŭtentigita uzanto, do tiel la nomo kaj retpoŝtadreo estas nurlegaj."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Afiŝita de %(user)s - %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marko"
+
+#: models.py:180
+msgid "date"
+msgstr "dato"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "komenta marko"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "komentaj markoj"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nova komento afiŝita \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobi komenton"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Ĉu certe publikigi ĉi-tiun komenton?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobi"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Dankon por la aprobo"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Dankon por trapasi tempon por plibonigi la diskutan kvaliton ĉe nia retejo"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Forigi komenton"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Ĉu certe forigi ĉi-tiun komenton?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Forigu"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Dankon por la forigo"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marki ĉi-tiun komenton"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Ĉu certe marki ĉi-tiun komenton?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marki"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Dankon por la marko"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Afiŝi"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Antaŭrigardo"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Dankon por al komentado"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Dankon por via komento"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Antaŭrigardi vian komenton"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publikigi vian komenton"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "aŭ lin redakti"
diff --git a/app/lib/django_comments/locale/es/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b598c9f
--- /dev/null
+++ b/app/lib/django_comments/locale/es/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/es/LC_MESSAGES/django.po b/app/lib/django_comments/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000..094a7f1
--- /dev/null
+++ b/app/lib/django_comments/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# David Martinez <davy.martinez@gmail.com>, 2016
+# Ernesto Avilés Vázquez <whippiii@gmail.com>, 2015
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Marc Garcia <garcia.marc@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-04-01 14:06+0000\n"
+"Last-Translator: David Martinez <davy.martinez@gmail.com>\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django-contrib-comments/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "contenido"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "metadatos"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcado"
+msgstr[1] "marcados"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar los comentarios seleccionados"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobado"
+msgstr[1] "aprobados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "aprobar los comentarios seleccionados"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminado"
+msgstr[1] "eliminados"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eliminar los comentarios seleccionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 comentarios ha sido %(action)s satisfactoriamente."
+msgstr[1] "%(count)s comentarios han sido %(action)s satisfactoriamente."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "comentarios de %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentarios en %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nombre"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "dirección de correo electrónico"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentario"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "¡Cuide su vocabulario! Aquí no admitimos la palabra %s."
+msgstr[1] "¡Cuide su vocabulario! Aquí no admitimos las palabras %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "y"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si introduce algo en este campo su comentario será tratado como spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de contenido"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID de objeto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuario"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nombre del usuario"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "dirección de correo electrónico del usuario"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL del usuario"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentario"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "fecha/hora de envío"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Dirección IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "es público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Desmarque esta casilla para hacer desaparecer el comentario del sitio web de forma efectiva."
+
+#: models.py:67
+msgid "is removed"
+msgstr "está eliminado"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marque esta opción si el comentario es inapropiado. En su lugar se mostrará el mensaje \"Este comentario ha sido eliminado\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentarios"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentario ha sido enviado por un usuario autentificado: de modo que su nombre no es modificable."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentario ha sido colocado por un usuario autentificado: de modo que su dirección de correo electrónico no es modificable."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Enviado por %(user)s en %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marcar"
+
+#: models.py:180
+msgid "date"
+msgstr "fecha"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marca de comentario"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcas de comentario"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nuevo comentario publicado en \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobar un comentario"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Realmente desea hacer este comentario público?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Gracias por aprobar"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Gracias por tomarse el tiempo para mejorar la calidad del debate en nuestro sitio"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eliminar un comentario"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "¿Realmente desea eliminar este comentario?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Gracias por eliminar"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar este comentario"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "¿Realmente desea marcar este comentario?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Graciar por marcar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Enviar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Previsualizar"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Gracias por comentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Gracias por su comentario"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Previsualizar su comentario"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Por favor, corrija el siguiente error"
+msgstr[1] "Por favor, corrija los siguientes errores"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Envie su comentario"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o haga cambios"
diff --git a/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c6fcebb
--- /dev/null
+++ b/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.po b/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e7be44f
--- /dev/null
+++ b/app/lib/django_comments/locale/es_AR/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Ramiro Morales, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-11-30 00:33+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/django/django-contrib-comments/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contenido"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadatos"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcado"
+msgstr[1] "marcados"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar comentarios seleccionados"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobado"
+msgstr[1] "aprobados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprobar comentario seleccionado"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminado"
+msgstr[1] "eliminados"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eliminar comentarios seleccionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "un comentario fue %(action)s satisfactoriamente."
+msgstr[1] "%(count)s comentarios fueron %(action)s satisfactoriamente"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "comentarios en %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentarios en %(site_name)s."
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nombre"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Dirección de correo electrónico"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentario"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "¡Controla tu lenguaje! Aquí no admitimos la palabra %s."
+msgstr[1] "¡Controla tu lenguaje! Aquí no admitimos las palabras %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "y"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si introduce algo en este campo su comentario será tratado como spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de contenido"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID de objeto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuario"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nombre de usuario"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "dirección de correo electrónico del usuario"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL del usuario"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentario"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "fecha/hora de envío"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Dirección IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "es público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "desmarque este ítem para que el comentario desaparezca del sitio."
+
+#: models.py:67
+msgid "is removed"
+msgstr "se ha eliminado"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marque este ítem si el comentario es inapropiado. En su lugar se mostrará un mensaje \"Este comentario ha sido eliminado\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentarios"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentario ha sido enviado por un usuario identificado, por lo tanto el nombre no puede modificarse."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentario ha sido enviado por un usuario identificado, por lo tanto la dirección de correo electrónico no puede modificarse."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Enviado por %(user)s el %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marca"
+
+#: models.py:180
+msgid "date"
+msgstr "fecha"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marca de comentario"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcas de comentario"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobar un comentario"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "¿Confirma que realmente desea hacer este comentario público?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "¡Gracias por aprobar!"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Gracias por tomarse el tiempo de mejorar la calidad de la discusión en nuestro sitio"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eliminar un comentario"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "¿Confirma que realmente desea eliminar este comentario?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "¡Gracias por eliminar!"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar este comentario"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "¿Confirma que realmente desde marcar este comentario?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "¡Gracias por marcar!"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Remitir"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Previsualización"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Gracias por dejar su comentario"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Gracias por dejar su comentario"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Ver una copia previa de su comentario"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Por favor corrija el siguiente error"
+msgstr[1] "Por favor corrija los siguientes errores"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Enviar su comentario"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o realice modificaciones"
diff --git a/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..8f2a0e5
--- /dev/null
+++ b/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.po b/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.po
new file mode 100644
index 0000000..627d283
--- /dev/null
+++ b/app/lib/django_comments/locale/es_MX/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Abraham Estrada <abraham.estrada@gmail.com>, 2011
+# guillermo Iglesias <ziberleon@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-08-26 17:50+0000\n"
+"Last-Translator: guillermo Iglesias <ziberleon@gmail.com>\n"
+"Language-Team: Spanish (Mexico) (http://www.transifex.com/django/django-contrib-comments/language/es_MX/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_MX\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contenido"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadatos"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcado"
+msgstr[1] "marcados"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar comentarios seleccionados"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobado"
+msgstr[1] "aprobados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprobar comentario seleccionado"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminado"
+msgstr[1] "eliminados"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eliminar comentarios seleccionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "%(count)s comentarios fueron %(action)s satisfactoriamente."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "comentarios en %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentarios en %(site_name)s "
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nombre"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Dirección de correo electrónico"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentario"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "¡Controla tu lenguaje! Aquí no admitimos la palabra %s."
+msgstr[1] "¡Controla tu lenguaje! Aquí no admitimos las palabras %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "y"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si introduce algo en este campo su comentario será tratado como spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de contenido"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID de objeto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuario"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nombre de usuario"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "dirección de correo electrónico del usuario"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL del usuario"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentario"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "fecha/hora de envío"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Dirección IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "es público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Desactive esta casilla para hacer el comentario desaparezca del sitio."
+
+#: models.py:67
+msgid "is removed"
+msgstr "se ha eliminado"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marque esta casilla si el comentario es inapropiado. En su lugar se mostrará un mensaje \"Este comentario ha sido eliminado\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentarios"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentario ha sido enviado por un usuario identificado, por lo tanto el nombre no puede modificarse."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentario ha sido enviado por un usuario identificado, por lo tanto la dirección de correo electrónico no puede modificarse."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Enviado por %(user)s el %(date)s \n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marca"
+
+#: models.py:180
+msgid "date"
+msgstr "fecha"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marca de comentario"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcas de comentario"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s]Nuevo comentario entregado el \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobar un comentario"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "¿Desea hacer público este comentario?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Gracias por aprovar"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Gracias por tomarse el tiempo para mejorar la calidad de la discución en nuestro sitio"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eliminar comentario"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "¿Desea eliminar este comentario?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Gracias por eliminar"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar este comentario"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "¿Desea marcar este comentario?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Gracias por marcar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Enviar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Previsualizar"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Gracias por comentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Gracier por el comentario"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Previsualizar el comentario"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Envia comentario"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o hacer cambios"
diff --git a/app/lib/django_comments/locale/et/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/et/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..af2c301
--- /dev/null
+++ b/app/lib/django_comments/locale/et/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/et/LC_MESSAGES/django.po b/app/lib/django_comments/locale/et/LC_MESSAGES/django.po
new file mode 100644
index 0000000..81d7356
--- /dev/null
+++ b/app/lib/django_comments/locale/et/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# madisvain <madisvain@gmail.com>, 2011
+# Martin <martinpajuste@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2016-01-28 09:43+0000\n"
+"Last-Translator: Martin <martinpajuste@gmail.com>\n"
+"Language-Team: Estonian (http://www.transifex.com/django/django-contrib-comments/language/et/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Sisu"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meta-andmed"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "märgistatud"
+msgstr[1] "märgistatud"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Märgista valitud kommentaarid"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "heaks kiidetud"
+msgstr[1] "heaks kiidetud"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Kiida heaks valitud kommentaarid"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eemaldatud"
+msgstr[1] "eemaldatud"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eemalda valitud kommentaarid"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s comments were successfully %(action)s."
+msgstr[1] "%(count)s comments were successfully %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Saidi %(site_name)s kommentaarid"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Viimased kommentaarid saidil %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nimi"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-posti aadress"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentaar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Jälgige oma keelekasutust. Sõna %s ei ole lubatud."
+msgstr[1] "Jälgige oma keelekasutust. Sõnad %s ei ole lubatud."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ja"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Kui sisestate sellesse lahtrisse midagi, loetakse kommentaar rämpsuks"
+
+#: models.py:23
+msgid "content type"
+msgstr "sisutüüp"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekti ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "kasutaja"
+
+#: models.py:55
+msgid "user's name"
+msgstr "kasutaja pärisnimi"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "kasutaja e-posti aadress"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "kasutaja URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentaar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "loomise kuupäev/kellaaeg"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP aadress"
+
+#: models.py:64
+msgid "is public"
+msgstr "on avalik"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Eemaldage siit linnuke, et varjata kommentaar saidilt."
+
+#: models.py:67
+msgid "is removed"
+msgstr "on eemaldatud"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Märkige siia linnuke, kui see kommentaar on ebasobiv. Kommentaari asemel kuvatakse kirja \"Kommentaar on kustutatud\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentaarid"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Selle kommentaari postitas sisselogitud kasutaja, mistõttu ei ole nimetus muudetav."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Selle kommentaari postitas sisselogitud kasutaja, mistõttu ei ole e-posti aadress muudetav."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postitatud kasutaja %(user)s poolt %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "märge"
+
+#: models.py:180
+msgid "date"
+msgstr "kuupäev"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentaari märge"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommentaari märked"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Märgi kommentaar sobivaks"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Oled kindel, et soovid teha selle kommentaari avalikuks?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Sobib"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Aitäh, et märkisid kommentaari sobivaks"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Aitäh, et leidsid aega parandamaks arutelude kvaliteeti meie lehel"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eemalda kommentaar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Oled kindel, et soovid selle kommentaari eemaldada?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eemalda"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Aitäh, et eemaldasid kommentaari"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Märgi see kommentaar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Oled kindel, et soovid selle kommentaari märkida?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Märge"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Aitäh, et märkisid kommentaari"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Postita"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Eelvaade"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Tänan kommenteerimast"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Aitäh kommentaari eest"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Kommentaari eelvaade"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Palun parandage allolev viga"
+msgstr[1] "Palun parandage allolevad vead"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Postita kommentaar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "või tee muudatused"
diff --git a/app/lib/django_comments/locale/eu/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/eu/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..5082c21
--- /dev/null
+++ b/app/lib/django_comments/locale/eu/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/eu/LC_MESSAGES/django.po b/app/lib/django_comments/locale/eu/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c89d967
--- /dev/null
+++ b/app/lib/django_comments/locale/eu/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Aitzol Naberan <anaberan@codesyntax.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Basque (http://www.transifex.com/django/django-contrib-comments/language/eu/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: eu\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Edukia"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metada"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "markaduna"
+msgstr[1] "markadunak"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Markatu aukeratutako iruzkinak"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "onartua"
+msgstr[1] "onartuak"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Onartu aukeratutako iruzkinak"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "ezabatua"
+msgstr[1] "ezabatuak"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Ezabatu aukeratutako iruzkinak"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "%(count)s iruzkin ondo %(action)s dira."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s guneko iruzkinak"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s guneko azken iruzkinak"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Eposta helbidea"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Iruzkina"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Txiiist! %s hitza ez zaigu gustatzen"
+msgstr[1] "Txiiist! %s hitzak ez zaizkigu gustatzen"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "eta"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Eremu honetan zerbait idazten baduzu zure iruzkina spam gisa tratatuko da."
+
+#: models.py:23
+msgid "content type"
+msgstr "eduki mota"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objetuaren ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "Erabiltzailea"
+
+#: models.py:55
+msgid "user's name"
+msgstr "erabiltzailearen izena"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "erabiltzailearen eposta helbidea"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "erabiltzailearen URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "iruzkina"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hordua bidalia"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP helbidea"
+
+#: models.py:64
+msgid "is public"
+msgstr "publikoa"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Markatu kutxa hau iruzkina webgunetik desagertarazteko."
+
+#: models.py:67
+msgid "is removed"
+msgstr "ezabatua"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Markatu kutxa hau komentario ezegokia bada. \"Komentario hau ezabatua izan da\" mezua erakutsiko da bere ordez."
+
+#: models.py:80
+msgid "comments"
+msgstr "iruzkinak"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Iruzkin hau autentikatutako erabiltzaile batek egin du. Hori dela eta, izena irakurtzeko moduan dago bakarrik. "
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Iruzkin hau autentikatutako erabiltzaile batek egin du. Hori dela eta, eposta irakurtzeko moduan dago bakarrik. "
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s erabiltzileak bidalia %(date)s datan\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marka"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "iruzkin marka"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "iruzkin markak"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Onartu iruzkina"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Publikatu iruzkina?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Onartu"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Eskerrik asko onartzearren"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Eskerrik asko webguneko estabaidaren kalitatea hobetzeko hartutako denboragatik"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Ezabatu iruzkina"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Ziur iruzkin hau ezabtu nahi duzula?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Ezabatu"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Eskerrik asko ezabatzearren"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Markatu iruzkina"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Ziur iruzkin hau markatu nahi duzula?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marka"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Eskerrik asko markatzearren"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Bidali"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Aurreikusi"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Eskerrik asko iruzkintzearren"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Eskerrik asko zure iruzkinagatik"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Aurreikusi zure iruzkina"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Bidali zure iruzkina"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "edo egin aldaketak"
diff --git a/app/lib/django_comments/locale/fa/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/fa/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e0e50df
--- /dev/null
+++ b/app/lib/django_comments/locale/fa/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/fa/LC_MESSAGES/django.po b/app/lib/django_comments/locale/fa/LC_MESSAGES/django.po
new file mode 100644
index 0000000..3dd700b
--- /dev/null
+++ b/app/lib/django_comments/locale/fa/LC_MESSAGES/django.po
@@ -0,0 +1,294 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Ali Nikneshan <ali@nikneshan.com>, 2012
+# Arash Fazeli <arash_fazeli77@yahoo.com>, 2012
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Kasra Ahmadvand <kasra.ahmadvand@gmail.com>, 2015
+# Pouya Abbassi, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-07-01 21:45+0000\n"
+"Last-Translator: Pouya Abbassi\n"
+"Language-Team: Persian (http://www.transifex.com/django/django-contrib-comments/language/fa/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fa\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "محتوا"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "فرا داده"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "پرچم دار"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "نشان‌گذاری نظرات انتخاب شده"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "تایید شده"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "تایید نظرات انتخاب شده"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "حذف شده"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "حذف نظر های انتخاب شده"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s کامنت با موفقیت %(action)s شدند."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "نظرات %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "آخرین نظرات در %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "نام"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "نشانی پست الکترونیکی"
+
+#: forms.py:98
+msgid "URL"
+msgstr "نشانی اینترنتی"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "نظر:"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "حرف دهنت رو بفهم! کلمهٔ %s اینجا قابل قبول نیست"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "و"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "اگر چیزی در این فیلد وارد کنید، نظر شما به عنوان اسپم شناخته خواهد شد"
+
+#: models.py:23
+msgid "content type"
+msgstr "نوع محتوا"
+
+#: models.py:25
+msgid "object ID"
+msgstr "مشخصهٔ شیء"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "کاربر"
+
+#: models.py:55
+msgid "user's name"
+msgstr "نام کاربر"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "نشانی پست الکترونیکی کاربر"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "نشانی اینترنتی کاربر"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "نظر"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "تاریخ/زمان فرستاده شد"
+
+#: models.py:63
+msgid "IP address"
+msgstr "نشانی IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "عمومی است"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "تیک این جعبه را بردارید تا نظر به طور کارا از وبگاه ناپدید شود."
+
+#: models.py:67
+msgid "is removed"
+msgstr "حذف شده است"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "اگر نظر نامناسب است این جا علامت بزنید. پیغام \"این نظر حذف شد\" به جای آن نمایش داده می شود."
+
+#: models.py:80
+msgid "comments"
+msgstr "نظرات"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "این نظر توسط یک کاربر ثبت‌شده فرستاده شده و لذا نامش فقط-خواندنی است."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "این نظر توسط یک کاربر ثبت‌شده فرستاده شده و لذا پست الکترونیکی‌اش فقط-خواندنی است."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "ارسال‌شده توسط %(user)s در تاریخ %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "علامت گذاری "
+
+#: models.py:180
+msgid "date"
+msgstr "تاریخ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "علامت گذاری نظر"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "علامت گذاری های نظر"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] نظر جدیدی در \"%(object)s\" ارسال شده است."
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "تایید یک نظر"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "واقعا این نظر به صورت عمومی نمایش داده شود؟"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "تایید"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "ممنون از تایید."
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "ممنون از وقتی که برای افزایش کیفیت بحث در سایت ما گذاشتید."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "حذف یک نظر"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "واقعا این نظر حذف شود؟"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "حذف"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "ممنون از حذف"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "علامت گذاری این نظر"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "واقعا این نظر علامت گذاری شود؟"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "علامت گذاری "
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "ممنون از علامت گذاری "
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "پست"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "پیش نمایش"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "برای اظهار نظر"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "با تشکر از نظر شما"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "پیش نمایش نظر شما"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "لطفا خطاهای زیر را تصحیح کنید"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "نظر خود را ارسال کنید"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "و یا تغییر ایجاد کنید."
diff --git a/app/lib/django_comments/locale/fi/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/fi/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..abaa70d
--- /dev/null
+++ b/app/lib/django_comments/locale/fi/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/fi/LC_MESSAGES/django.po b/app/lib/django_comments/locale/fi/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ad81e19
--- /dev/null
+++ b/app/lib/django_comments/locale/fi/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Finnish (http://www.transifex.com/django/django-contrib-comments/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Sisältö"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metatieto"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "merkitty"
+msgstr[1] "merkitty"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Merkitse valitut kommentit"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "hyväksytty"
+msgstr[1] "hyväksytty"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Hyväksy valitut kommentit"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "poistettu"
+msgstr[1] "poistettu"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Poista valitut kommentit"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 kommentti %(count)s onnistuneesti."
+msgstr[1] "%(count)s kommenttia %(action)s onnistuneesti."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Kommentit sivustolle %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Sivuston %(site_name)s viimeisimmät kommentit"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Sähköpostiosoite"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL-osoite"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentti"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Siivoa suusi! Sanaa \"%s\" ei saa käyttää tässä."
+msgstr[1] "Siivoa suusi! Sanoja \"%s\" ei saa käyttää tässä."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ja"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Jos syötät tähän kenttään jotain, kommenttisi luokitellaan roskapostiksi"
+
+#: models.py:23
+msgid "content type"
+msgstr "sisältötyyppi"
+
+#: models.py:25
+msgid "object ID"
+msgstr "kohteen tunniste"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "käyttäjä"
+
+#: models.py:55
+msgid "user's name"
+msgstr "käyttäjän nimi"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "käyttäjän sähköpostiosoite"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "käyttäjän URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentti"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "lähettämishetki"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-osoite"
+
+#: models.py:64
+msgid "is public"
+msgstr "on julkinen"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Piilottaaksesi kommentin näkymästä sivustolta, poista tämä ruksi."
+
+#: models.py:67
+msgid "is removed"
+msgstr "on poistettu"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Rastita jos kommentti on asiaankuulumaton. Kommentin tilalla näytetään\nviesti \"Tämä kommentti on poistettu\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentit"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Kommentin lähettäjän nimeä ei voi muuttaa, koska lähettäjä on kirjautunut käyttäjä."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Kommentin lähettäjän sähköpostiosoitetta ei voi muuttaa, koska lähettäjä on kirjautunut käyttäjä."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr " Kirjoittanut %(user)s, pvm %(date)s\\n\n \\n\n %(comment)s\\n\n \\n\n http://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "merkintä"
+
+#: models.py:180
+msgid "date"
+msgstr "päivä"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentin merkintä"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommenttien merkinnät"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Hyväksy kommentti"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Haluatko varmasti tehdä kommentista julkisen?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Hyväksy"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Kiitos hyväksynnästäsi"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Kiitos sivustomme keskusteluihin panostamastasi ajasta"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Poista kommentti"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Haluatko varmasti poistaa tämän kommentin?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Poista"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Kiitos poistamisesta"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Merkitse tämä kommentti"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Haluatko varmasti merkitä tämän kommentin?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Merkitse"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Kiitos merkitsemisestä"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Lähetä"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Esikatsele"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Kiitos kommentista"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Kiitos kommentistasi"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Esikatsele kommenttia"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Lähetä kommentti"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "tai tee muutoksia"
diff --git a/app/lib/django_comments/locale/fr/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..331dde1
--- /dev/null
+++ b/app/lib/django_comments/locale/fr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/fr/LC_MESSAGES/django.po b/app/lib/django_comments/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..d35d099
--- /dev/null
+++ b/app/lib/django_comments/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,298 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Claude Paroz <claude@2xlibre.net>, 2011
+# Claude Paroz <claude@2xlibre.net>, 2015-2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-03-29 19:22+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: French (http://www.transifex.com/django/django-contrib-comments/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contenu"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Métadonnées"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marqué"
+msgstr[1] "marqués"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marquer les commentaires sélectionnés"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "approuvé"
+msgstr[1] "approuvés"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Approuver les commentaires sélectionnés"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "supprimé"
+msgstr[1] "supprimés"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Masquer les commentaires sélectionnés"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 commentaire a été %(action)s avec succès."
+msgstr[1] "%(count)s commentaires ont été %(action)ss avec succès."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Commentaires sur %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Derniers commentaires sur %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nom"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adresse électronique"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Commentaire"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Attention à votre langage ! Le terme %s n'est pas autorisé ici."
+msgstr[1] "Attention à votre langage ! Les termes %s ne sont pas autorisés ici."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "et"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si vous saisissez quelque chose dans ce champ, votre commentaire sera considéré comme étant indésirable"
+
+#: models.py:23
+msgid "content type"
+msgstr "type de contenu"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID de l'objet"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "utilisateur"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nom de l'utilisateur"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adresse électronique de l'utilisateur"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL de l'utilisateur"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "commentaire"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "date et heure soumises"
+
+#: models.py:63
+msgid "IP address"
+msgstr "adresse IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "est public"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Décochez cette case pour faire vraiment disparaître ce commentaire du site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "est masqué"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Cochez cette case si le commentaire est inadéquat. Un message type « Ce commentaire a été supprimé » sera affiché en lieu et place de celui-ci."
+
+#: models.py:80
+msgid "comments"
+msgstr "commentaires"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ce commentaire a été posté par un utilisateur authentifié, le nom est donc en lecture seule."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ce commentaire a été posté par un utilisateur authentifié et le courriel est donc en lecture seule"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Posté par %(user)s le %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "indicateur"
+
+#: models.py:180
+msgid "date"
+msgstr "date"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "indicateur de commentaire"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "indicateurs de commentaire"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nouveau commentaire envoyé pour « %(object)s »"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Valider un commentaire"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Voulez-vous rendre ce commentaire public ?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Valider"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Merci pour cette validation"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Merci d'avoir pris le temps d'améliorer la qualité de la discussion sur notre site"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Supprimer un commentaire"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Voulez-vous supprimer définitivement ce commentaire ?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Supprimer"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Merci pour cette suppression"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Signaler ce commentaire"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Voulez-vous vraiment signaler ce commentaire ?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Signaler"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Merci d'avoir signalé ce commentaire"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Envoyer"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Prévisualiser"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Merci pour votre commentaire"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Merci pour votre commentaire"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Prévisualiser votre commentaire"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Veuillez corriger l'erreur suivante."
+msgstr[1] "Veuillez corriger les erreurs suivantes."
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Envoyer votre commentaire"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ou le modifier"
diff --git a/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a95bd48
--- /dev/null
+++ b/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.po b/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.po
new file mode 100644
index 0000000..065687c
--- /dev/null
+++ b/app/lib/django_comments/locale/fy_NL/LC_MESSAGES/django.po
@@ -0,0 +1,290 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Western Frisian (Netherlands) (http://www.transifex.com/django/django-contrib-comments/language/fy_NL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fy_NL\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr ""
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr ""
+
+#: forms.py:99
+msgid "Comment"
+msgstr ""
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr ""
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr ""
+
+#: models.py:25
+msgid "object ID"
+msgstr ""
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr ""
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr ""
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr ""
+
+#: models.py:63
+msgid "IP address"
+msgstr ""
+
+#: models.py:64
+msgid "is public"
+msgstr ""
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr ""
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr ""
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr ""
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/ga/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ga/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..931df81
--- /dev/null
+++ b/app/lib/django_comments/locale/ga/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ga/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ga/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8e4d69f
--- /dev/null
+++ b/app/lib/django_comments/locale/ga/LC_MESSAGES/django.po
@@ -0,0 +1,310 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Michael Thornhill <michael@maithu.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Irish (http://www.transifex.com/django/django-contrib-comments/language/ga/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ga\n"
+"Plural-Forms: nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Inneachar"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meiteashonraí"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "bratach curtha leis "
+msgstr[1] "bratach curtha leis "
+msgstr[2] "bratach curtha leis "
+msgstr[3] "bratach curtha leis "
+msgstr[4] "bratach curtha leis "
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Bratach nótaí tráchta roghnaithe"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "ceadaithe"
+msgstr[1] "ceadaithe"
+msgstr[2] "ceadaithe"
+msgstr[3] "ceadaithe"
+msgstr[4] "ceadaithe"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Cheadú nótaí tráchta roghnaithe"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "bhaint"
+msgstr[1] "bhaint"
+msgstr[2] "bhaint"
+msgstr[3] "bhaint"
+msgstr[4] "bhaint"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Bain nótaí tráchta roghnaithe"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "Bhí %(count)s nóta tráchta %(action)s go rathúil."
+msgstr[1] "Bhí %(count)s nótaí tráchta %(action)s go rathúil."
+msgstr[2] "Bhí %(count)s nótaí tráchta %(action)s go rathúil."
+msgstr[3] "Bhí %(count)s nótaí tráchta %(action)s go rathúil."
+msgstr[4] "Bhí %(count)s nótaí tráchta %(action)s go rathúil."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s nótaí"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Nótaí tráchtaí is déanaí ar %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "R-phost"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Nóta tráchta"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Féach ar do bhéal! Níl an focal %s cheadaítear anseo."
+msgstr[1] "Féach ar do bhéal! Níl na focail %s cheadaítear anseo."
+msgstr[2] "Féach ar do bhéal! Níl na focail %s cheadaítear anseo."
+msgstr[3] "Féach ar do bhéal! Níl na focail %s cheadaítear anseo."
+msgstr[4] "Féach ar do bhéal! Níl na focail %s cheadaítear anseo."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "agus"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Má cuireann tú aon rud sa réimse seo, beidh do nóta déileálfar mar spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tíopa inneachar "
+
+#: models.py:25
+msgid "object ID"
+msgstr "oibiacht ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "úsáideoir"
+
+#: models.py:55
+msgid "user's name"
+msgstr "Ainm úsáideoir"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "seoladh r-phost an t-úsáideoir"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL an t-úsáideora"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "trácht"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "Dáta/am curtha isteach"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Seol IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "poiblí"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Díthiceáil an bosca seo chun an nóta a thógáil as an suíomh."
+
+#: models.py:67
+msgid "is removed"
+msgstr "Scrioste"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Seic an bosca seo dá bbéadh an nóta tráchta seo míchuí. Taispeantar \"Bhí an nóta tráchta scrioste\" in áit an nóta tráchta seo."
+
+#: models.py:80
+msgid "comments"
+msgstr "nótaí tráchta"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Bhí an nóta tráchta póstailte trí uaire trí úsáideoir fíordheimhnithe mar sin tá an ainm léamh-amhain."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Bhí an nóta tráchta póstailte trí úsáideoir fíordeimhnite mar sin tá an r-phost léamh amháin."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postáilte trí %(user)s ar %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "brat"
+
+#: models.py:180
+msgid "date"
+msgstr "dáta"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "brat nóta tráchta"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "bratacha nótaí tráchta"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Ceadaigh nóta tráchta"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Cuir an nóta seo poiblí?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Fhormheas"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Go raibh maith agait le hadhaigh to formheas"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Go raibh maith agat as an am chun feabhas a chur ar chaighdeán na díospóireachta ar ár suíomh"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Tóg amach nóta tráchta"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Dáiríre, cuir amach an nóta seo?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Tóg amach"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Go raibh maith agat le hadhaigh do thógail amach"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Cuir brat ar an nóta tráchta seo"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Go deimhin cuir brat ar an nóta tráchta seo?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Brat"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Go raibh maith agat le hadhaigh do brat"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Post"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Réamhamharc"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Go raibh maith agat le hadhaign do nóta tráchta"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Go raibh maith agat le hadhaigh do nóta tráchta"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Nóta tráchta réamhamharc"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Seol do Nóta tráchta"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "nó déan aithraithe"
diff --git a/app/lib/django_comments/locale/gl/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/gl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..88ed707
--- /dev/null
+++ b/app/lib/django_comments/locale/gl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/gl/LC_MESSAGES/django.po b/app/lib/django_comments/locale/gl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ae668bf
--- /dev/null
+++ b/app/lib/django_comments/locale/gl/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# fasouto <fsoutomoure@gmail.com>, 2011
+# fonso <fonzzo@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Galician (http://www.transifex.com/django/django-contrib-comments/language/gl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: gl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contido"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadatos"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "con indicador"
+msgstr[1] "con indicador"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Poñer un indicador"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobado"
+msgstr[1] "aprobados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprobar os comentarios seleccionados"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminado"
+msgstr[1] "eliminados"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Eliminar os comentarios seleccionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "%(count)s comentarios foron %(action)s con éxito."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Comentarios en %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentarios en %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Enderezo electrónico"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentario"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Sen palabrotas, por favor! Aquí non se pode usar a palabra %s."
+msgstr[1] "Sen palabrotas, por favor! Aquí non se poden usar as palabras %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "e"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Se insire calquera cousa neste campo o seu comentario será tratado coma spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de contido"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID do obxecto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuario"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nome de usuario"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "enderedo electrónico do usuario"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL do usuario"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentario"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hora do envío"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Enderezo IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "é público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Desmarque esta casilla para eliminar o comentario definitivamente deste sitio."
+
+#: models.py:67
+msgid "is removed"
+msgstr "está borrado"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marque esta caixa se o comentario non é apropiado. Verase a mensaxe \"Este comentario foi borrado\" no canto do seu contido."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentarios"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentario foi publicado por un usuario identificado e polo tanto o nome é de só lectura."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentario foi publicado por un usuario identificado e polo tanto o enderezo de correo electrónico é de só lectura."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Publicado por %(user)s o %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "indicador"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "indicador de comentarios"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "indicadores de comentarios"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobar un comentario"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Realmente desexa facer público este comentario?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Grazas pola aprobación"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Grazas por tomar o tempo de mellorar a calidade da discusión no noso sitio"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Eliminar un comentario"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Realmente desexa eliminar este comentario?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Eliminar"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Grazas pola eliminación"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Poñerlle un indicador a este comentario"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Realmente desexa poñerlle un indicador a este comentario?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Indicador"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Grazas por colocar o indicador"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publicar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Vista previa"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Grazas polo seu comentario"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Grazas polo seu comentario"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Vista previa do seu comentario"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publicar o seu comentario"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ou facer cambios"
diff --git a/app/lib/django_comments/locale/he/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/he/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..8ec3550
--- /dev/null
+++ b/app/lib/django_comments/locale/he/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/he/LC_MESSAGES/django.po b/app/lib/django_comments/locale/he/LC_MESSAGES/django.po
new file mode 100644
index 0000000..939c38b
--- /dev/null
+++ b/app/lib/django_comments/locale/he/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Meir Kriheli <mkriheli@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-11-28 20:13+0000\n"
+"Last-Translator: Meir Kriheli <mkriheli@gmail.com>\n"
+"Language-Team: Hebrew (http://www.transifex.com/django/django-contrib-comments/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "תוכן"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "מטא־נתונים"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "סומנה"
+msgstr[1] "סומנו"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "סמן תגובות שנבחרו"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "אושרה"
+msgstr[1] "אושרו"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "אשר תגובות שנבחרו"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "הוסרה"
+msgstr[1] "הוסרו"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "הסר תגובות שנבחרו"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "תגובה אחת %(action)s בהצלחה"
+msgstr[1] "%(count)s תגובות %(action)s בהצלחה"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "תגובות עבור %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "התגובות האחרונות על %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "שם"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "כתובת דוא\"ל"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "תגובה"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "שמור על לשונך! המילה %s אסורה לשימוש כאן."
+msgstr[1] "שמור על לשונך! המילים %s אסורות לשימוש כאן."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ו"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "אם יוזן משהו בשדה זה תגובתך תטופל כספאם"
+
+#: models.py:23
+msgid "content type"
+msgstr "סוג תוכן"
+
+#: models.py:25
+msgid "object ID"
+msgstr "מזהה אובייקט"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "משתמש"
+
+#: models.py:55
+msgid "user's name"
+msgstr "שם משתמש"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "כתובת דוא\"ל משתמש"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "אתר המשתמש"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "תגובה"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "תאריך/שעת הגשה"
+
+#: models.py:63
+msgid "IP address"
+msgstr "כתובת IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "פומבי "
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "ביטול סימון התיבה יעלים בפועל את התגובה מהאתר"
+
+#: models.py:67
+msgid "is removed"
+msgstr "האם הוסר"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "יש לסמן תיבה זו עבור תגובה לא נאותה. הודעת \"תגובה זו נמחקה\" תוצג במקום התגובה."
+
+#: models.py:80
+msgid "comments"
+msgstr "תגובות"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "הודעה זו נשלחה ע\"י משתמש מחובר לכן השם אינו ניתן לשינוי."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "הודעה זו נשלחה ע\"י משתמש מחובר לכן כתובת הדוא\"ל אינה ניתנת לשינוי."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "הוגש ע\"י %(user)s ב %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "סימן"
+
+#: models.py:180
+msgid "date"
+msgstr "תאריך"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "סמן הערה"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "סמני הערה"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "אשר הערה"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "באמת להפוך את התגובה לפומבית?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "אשר"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "תודה על אישור התגובה"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "תודה על שהקדשת מזמנך כדי לשפר את איכות הדיון באתר שלנו"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "הסר תגובה"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "באמת להסיר תגובה זו?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "להסיר"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "תודה על ההסרה"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "סמן תגובה זו"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "באמת לסמן תגובה זו?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "סימן"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "תודה על הסימון"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "פוסט"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "תצוגה מקדימה"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "תודה על התגובה"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "תודה על התגובה"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "תצוגה מקדימה של התגובה"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "נא לתקן את השגיה למטה"
+msgstr[1] "נא לתקן את השגיאות למטה"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "פרסם את התגובה"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "או לבצע שינויים"
diff --git a/app/lib/django_comments/locale/hi/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/hi/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e9384f8
--- /dev/null
+++ b/app/lib/django_comments/locale/hi/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/hi/LC_MESSAGES/django.po b/app/lib/django_comments/locale/hi/LC_MESSAGES/django.po
new file mode 100644
index 0000000..925849b
--- /dev/null
+++ b/app/lib/django_comments/locale/hi/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Sandeep Satavlekar <sandysat@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Hindi (http://www.transifex.com/django/django-contrib-comments/language/hi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "विषय सूची"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "मेटाडाटा"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "झंडी ऊंचायी"
+msgstr[1] "झंडी ऊंचायी"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "चयनित टिप्पणियों के लिए झंडी ऊंचाओ"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "स्वीकृत"
+msgstr[1] "स्वीकृत"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "चयनित टिप्पणियों को स्वीकार करो"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "हटाया"
+msgstr[1] "हटाया"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "चयनित टिप्पणियाँ हटाएँ"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s टिप्पणि सफलतापूर्वक %(action)s"
+msgstr[1] "%(count)s टिप्पणियाँ सफलतापूर्वक %(action)s"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s टिप्पणियाँ "
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s पर नवीनतम टिप्पणियाँ"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ईमेल पता"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "टिप्पणी"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "अपनी ज़बान संभालो %s यह शब्द इस्तेमाल करने की यहाँ अनुमति नहीं हैं "
+msgstr[1] "अपनी ज़बान संभालो %s यह शब्द इस्तेमाल करने की यहाँ अनुमति नहीं हैं "
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "और"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "अगर आप इस क्षेत्र में कुछ भी दर्ज करते हो तो आप की टिप्पणी के साथ spam के जैसा सुलुख किया जायेगा"
+
+#: models.py:23
+msgid "content type"
+msgstr "विषय-सूची प्रकार"
+
+#: models.py:25
+msgid "object ID"
+msgstr "वस्तु आइ डी"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "उपभोक्ता"
+
+#: models.py:55
+msgid "user's name"
+msgstr "प्रयोक्ता नाम"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "प्रयोक्ता ईमेल पता"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "प्रयोक्ता यू.आर.एल"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "टिप्पणी"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "तिथि/समय निवेदित"
+
+#: models.py:63
+msgid "IP address"
+msgstr "आइ.पि पता"
+
+#: models.py:64
+msgid "is public"
+msgstr "सार्वजनिक है"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "इस टिप्पणी को प्रभावी रूप से साइट से गायब करने के लिए यह बॉक्स को अनचेक करें."
+
+#: models.py:67
+msgid "is removed"
+msgstr "हटाया गया"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "अगर टिप्पणी अनुचित है तो इस बॉक्स को चेक करें. एक \"यह टिप्पणी हटा दी गयी हैं\" संदेश प्रदर्शित किया जाएगा."
+
+#: models.py:80
+msgid "comments"
+msgstr "टिप्पणियाँ"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "इस टिप्पणी को किसी प्राधिकृत उपयोगकर्ता द्वारा पोस्ट किया गया था और इसीलिए इस नाम को केवल पढ़ने के लिए है."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "इस टिप्पणी को किसी प्राधिकृत उपयोगकर्ता द्वारा पोस्ट किया गया था और इसीलिए यह ईमेल केवल पढ़ने के लिए है."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s द्वारा %(date)s पर पोस्ट की गयी हैं\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "झंडा"
+
+#: models.py:180
+msgid "date"
+msgstr "तिथि"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "टिप्पणी झंडा"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "टिप्पणी झंडे"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "टिप्पणी पसंद करें"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "क्या इस टिप्पणी को सार्वजनिक बनाएँ ?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "पसंद करें"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "पसंद करने के लिए धन्यवाद"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "हमारी साइट पर चर्चा की गुणवत्ता में सुधार के लिए समय देने के लिए धन्यवाद"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "टिप्पणी निकालें"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "क्या आप इस टिप्पणी को हटाना चाहते हैं ?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "निकालें"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "निकालने के लिये धन्यवाद"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "टिप्पनी को फ्लैग करो"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "क्या आप इस टिप्पणी को फ्लैग करना चाहते हैं ?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "फ्लैग"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "फ्लैग करने के लिए धन्यवाद"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "प्रस्तुत"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "पूर्व दर्शन"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "टिप्पणी के लिये धन्यवाद"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "टिप्पणी के लिये धन्यवाद"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "आपके टिप्पणी का पूर्व दर्शन देखे`"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "टिप्पणी प्रस्तुत करें"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "अथवा बदलें"
diff --git a/app/lib/django_comments/locale/hr/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/hr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..4408d09
--- /dev/null
+++ b/app/lib/django_comments/locale/hr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/hr/LC_MESSAGES/django.po b/app/lib/django_comments/locale/hr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4278981
--- /dev/null
+++ b/app/lib/django_comments/locale/hr/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Croatian (http://www.transifex.com/django/django-contrib-comments/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Sadržaj"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "oznaka"
+msgstr[1] "oznake"
+msgstr[2] "oznake"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označi ovaj komentar"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "odobreno"
+msgstr[1] "odobrene"
+msgstr[2] "odobrene"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Odobri odabrane komentare"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "uklonjeno"
+msgstr[1] "uklonjena"
+msgstr[2] "uklonjena"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Ukloni odabrane komentare"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentar je uspješno %(action)s."
+msgstr[1] "%(count)s komentara su uspješno %(action)s."
+msgstr[2] "%(count)s komentara su uspješno %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "komentari za %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Najnoviji komentari na %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mail adresa"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pazite na izražavanje! Riječ %s nije dopuštena."
+msgstr[1] "Pazite na izražavanje! Riječi %s nisu dopuštene."
+msgstr[2] "Pazite na izražavanje! Riječi %s nisu dopuštene."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "i"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ako unesete nešto u ovo polje vaš komentar biti će tretiran kao spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tip sadržaja"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID objekta"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "korisnik"
+
+#: models.py:55
+msgid "user's name"
+msgstr "korisničko ime"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "e-mail adresa korisnika"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "korisnikov URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum/vrijeme unosa"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresa"
+
+#: models.py:64
+msgid "is public"
+msgstr "javno dostupno"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Uklonite oznaku da bi komentar nestao sa stranica."
+
+#: models.py:67
+msgid "is removed"
+msgstr "uklonjeno"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Uključite ako je komentar neprikladan. Umjesto komentara biti će prikazana poruka \"Komentar je uklonjen.\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentari"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ovaj komentar je napisao prijavljeni korisnik te se ime ne može mijenjati.\n\n%(text)s"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ovaj komentar je napisao prijavljeni korisnik te se email ne može mijenjati.\n\n%(text)s"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Napisao %(user)s dana %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "oznaka"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "oznaka za komentar"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "oznake komentara"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Odobri komentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Učini komentar javno dostupnim?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Odobri"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Hvala na odobrenju"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Hvala što ste izdvojili vrijeme da poboljšate kvalitetu rasprava na stranicama"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Ukloni komentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Stvarno ukloni ovaj komentar?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Ukloni"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Hvala na brisanju"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označi ovaj komentar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Stvarno označi ovaj komentar?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Oznaka"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Hvala na označavanju"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Unos"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Prikaz"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Hvala što ste komentirali"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Hvala na komentaru"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Prikaz komentara"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Objava komentara"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ili unesite promjene"
diff --git a/app/lib/django_comments/locale/hu/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/hu/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..5af464e
--- /dev/null
+++ b/app/lib/django_comments/locale/hu/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/hu/LC_MESSAGES/django.po b/app/lib/django_comments/locale/hu/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c756190
--- /dev/null
+++ b/app/lib/django_comments/locale/hu/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Szilveszter Farkas <szilveszter.farkas@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Hungarian (http://www.transifex.com/django/django-contrib-comments/language/hu/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Tartalom"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metaadat"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "megjelölve"
+msgstr[1] "megjelölve"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Kiválasztott hozzászólások megjelölése"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "jóváhagyva"
+msgstr[1] "jóváhagyva"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Kiválasztott hozzászólások jóváhagyása"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "törölve"
+msgstr[1] "törölve"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Kiválasztott hozzászólások törlése"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s hozzászólás sikeresen %(action)s."
+msgstr[1] "%(count)s hozzászólás sikeresen %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s hozzászólások"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s legfrissebb hozzászólásai"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mail cím"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Hozzászólás"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Vigyázzon a szájára! Az ilyen szó (%s) itt nem megengedett."
+msgstr[1] "Vigyázzon a szájára! Az ilyen szavak (%s) itt nem megengedettek."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "és"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ha bármit begépel ebbe a mezőbe, akkor azt szemétként fogja kezelni a rendszer"
+
+#: models.py:23
+msgid "content type"
+msgstr "tartalom típusa"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objektum ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "felhasználó"
+
+#: models.py:55
+msgid "user's name"
+msgstr "felhasználó neve"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "felhasználó e-mail címe"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "felhasználó URL-je"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "megjegyzés"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dátum/idő beállítva"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP cím"
+
+#: models.py:64
+msgid "is public"
+msgstr "publikus"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Vegye ki a pipát a jelölőnégyzetből, hogy eltűntesse a hozzászólást az oldalról."
+
+#: models.py:67
+msgid "is removed"
+msgstr "eltávolítva"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Jelöld be a négyzetet, ha a megjegyzés nem megfelelő. Az \"Ezt a megjegyzést törölték\" üzenet fog megjelenni helyette."
+
+#: models.py:80
+msgid "comments"
+msgstr "hozzászólások"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ezt a hozzászólást egy hitelesített felhasználó küldte be, ezért a név csak olvasható."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ezt a hozzászólást egy hitelesített felhasználó küldte be, ezért az e-mail csak olvasható."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Beküldte %(user)s ekkor: %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "megjelölés"
+
+#: models.py:180
+msgid "date"
+msgstr "dátum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "hozzászólás megjelölés"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "hozzászólás megjelölés"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Hozzászólás jóváhagyása"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Biztosan publikálni szeretné ezt a hozzászólást?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Jóváhagyás"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Köszönjük a jóváhagyást"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Köszönjük, hogy időt szánt az oldalunkon zajló beszélgetések minőségének javítására"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Hozzászólás törlése"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Biztosan törli ezt a hozzászólást?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Törlés"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Köszönjük a törlést"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Hozzászólás megjelölése"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Biztosan megjelöli ezt a hozzászólást?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Megjelölés"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Köszönjük a megjelölést"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Elküldés"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Előnézet"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Köszönjük a hozzászólást"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Köszönjük, hogy hozzászólt"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Hozzászólás előnézetének megtekintése"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Hozzászólás elküldése"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "vagy módosítása"
diff --git a/app/lib/django_comments/locale/ia/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ia/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e707153
--- /dev/null
+++ b/app/lib/django_comments/locale/ia/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ia/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ia/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7639091
--- /dev/null
+++ b/app/lib/django_comments/locale/ia/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Martijn Dekker <mcdutchie@hotmail.com>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Interlingua (http://www.transifex.com/django/django-contrib-comments/language/ia/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ia\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contento"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadatos"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcate"
+msgstr[1] "marcate"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar le commentos seligite"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "approbate"
+msgstr[1] "approbate"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Approbar le commentos seligite"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "removite"
+msgstr[1] "removite"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Remover le commentos seligite"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 commento ha essite %(action)s con successo."
+msgstr[1] "%(count)s commentos ha essite %(action)s con successo."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Commentos de %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Ultime commentos in %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adresse de e-mail"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Commento"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Le parola %s non es permittite hic."
+msgstr[1] "Le parolas %s non es permittite hic."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "e"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Si tu insere qualcosa in iste campo, tu commento essera tractate como spam."
+
+#: models.py:23
+msgid "content type"
+msgstr "typo de contento"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID del objecto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usator"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nomine del usator"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adresse de e-mail del usator"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL del usator"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "commento"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hora de submission"
+
+#: models.py:63
+msgid "IP address"
+msgstr "adresse IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "es public"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Dismarca iste quadro pro facer le commento effectivemente disparer de iste sito."
+
+#: models.py:67
+msgid "is removed"
+msgstr "es removite"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Marca iste quadro si le commento es inappropriate. Un message \"iste commento ha essite removite\" essera monstrate in su loco."
+
+#: models.py:80
+msgid "comments"
+msgstr "commentos"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Iste commento ha essite publicate per un usator authenticate e dunque le nomine es immodificabile."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Iste commento ha essite publicate per un usator authenticate e dunque le adresse de e-mail es immodificabile."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Publicate per %(user)s le %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marca"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marcation de commento"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcationes de commento"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Approbar un commento"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Es tu secur de voler render iste commento public?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Approbar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Gratias pro approbar"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Gratias pro prender le tempore pro meliorar le qualitate del discussion in nostre sito"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Remover un commento"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Es tu secur de voler remover iste commento?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Remover"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Gratias pro remover"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar iste commento"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Es tu secur de voler marcar iste commento?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Gratias pro marcar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publicar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Previsualisar"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Gratias pro commentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Gratias pro tu commento"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Previsualisar tu commento"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publicar tu commento"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o facer modificationes"
diff --git a/app/lib/django_comments/locale/id/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/id/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..d6fae0a
--- /dev/null
+++ b/app/lib/django_comments/locale/id/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/id/LC_MESSAGES/django.po b/app/lib/django_comments/locale/id/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a7c4bea
--- /dev/null
+++ b/app/lib/django_comments/locale/id/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Fery Setiawan <gembelweb@gmail.com>, 2015-2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+# rodin <romihardiyanto@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-08-23 04:31+0000\n"
+"Last-Translator: Fery Setiawan <gembelweb@gmail.com>\n"
+"Language-Team: Indonesian (http://www.transifex.com/django/django-contrib-comments/language/id/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: id\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Isi"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "ditandai"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Tandai komentar terpilih"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "disetujui"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Setujui komentar terpilih"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "dihapus"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Hapus komentar terpilih"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentar berhasil %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "komentar pada %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Komentar terbaru pada %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nama"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Alamat email"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Awas! Kata %s tidak diizinkan di sini."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "dan"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Jika Anda mengisi bidang ini, komentar Anda akan dianggap sebagai spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipe konten"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID objek"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "pengguna"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nama pengguna"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "alamat email pengguna"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL pengguna"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "tanggal/waktu dikirim"
+
+#: models.py:63
+msgid "IP address"
+msgstr "alamat IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "untuk umum"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Hapus centang pada kotak ini agar komentar tidak ditampilkan pada situs."
+
+#: models.py:67
+msgid "is removed"
+msgstr "telah dihapus"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Centang kotak ini jika komentar tidak pantas. Pesan \"Komentar ini telah dihapus\" akan ditampilkan sebagai penggantinya."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentar"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Komentar ini dikirim oleh seorang pengguna terautentikasi sehingga nama pengguna tidak dapat diubah."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Komentar ini dikirim oleh seorang pengguna terautentikasi sehingga alamat email tidak dapat diubah."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Dikirim oleh %(user)s pada %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "tanda"
+
+#: models.py:180
+msgid "date"
+msgstr "tanggal"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "tanda komentar"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "tanda komentar"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Komentar baru ditempatkan pada \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Setujui komentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Yakin ingin menampilkan komentar ini untuk umum?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Setujui"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Terima kasih telah menyetujui komentar"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Terima kasih telah membantu meningkatkan kualitas diskusi pada situs kami"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Hapus komentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Yakin ingin menghapus komentar ini?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Hapus"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Terima kasih telah menghapus"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Tandai komentar ini"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Yakin ingin menandai komentar ini?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Tandai"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Terima kasih telah menandai"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Kirim"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Pratinjau"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Terima kasih telah meninggalkan komentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Terima kasih atas komentar Anda"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Pratinjau komentar Anda"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Mohon perbaiki kesalahan di bawah ini"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Kirim komentar Anda"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "atau lakukan perubahan"
diff --git a/app/lib/django_comments/locale/is/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/is/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..bf5e2cd
--- /dev/null
+++ b/app/lib/django_comments/locale/is/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/is/LC_MESSAGES/django.po b/app/lib/django_comments/locale/is/LC_MESSAGES/django.po
new file mode 100644
index 0000000..fee1eda
--- /dev/null
+++ b/app/lib/django_comments/locale/is/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Hafsteinn Einarsson <haffi67@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Icelandic (http://www.transifex.com/django/django-contrib-comments/language/is/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: is\n"
+"Plural-Forms: nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Innihald"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Hengigögn"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flaggað"
+msgstr[1] "flaggað"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Flagga valdar athugasemdir"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "samþykkt"
+msgstr[1] "samþykkt"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Samþykkja valdar athugasemdir"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "fjarlægð"
+msgstr[1] "fjarlægðar"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Fjarlægja valdar athugasemdir"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "Það tókst að %(action)s %(count)s athugasemdum."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s: athugasemdir"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Nýjustu athugasemdir á %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Netfang"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Veffang"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Athugasemd"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Passaðu orðbragðið! Orðið %s er ekki leyft hér."
+msgstr[1] "Passaðu orðbragðið! Orðin %s eru ekki leyfð hér."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "og"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ef þú skrifar eitthvað hérna verður athugasemdin sjálfkrafa meðhöndluð sem ruslpóstur"
+
+#: models.py:23
+msgid "content type"
+msgstr "efnistag"
+
+#: models.py:25
+msgid "object ID"
+msgstr "kenni hlutar"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "notandi"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nafn notanda"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "netfang notanda"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "veffang notanda"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "athugasemd"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "innsent dags/tími"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP tala"
+
+#: models.py:64
+msgid "is public"
+msgstr "er öllum sýnilegt"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Taktu hakið úr til að fjarlægja athugasemdina af vefsíðunni."
+
+#: models.py:67
+msgid "is removed"
+msgstr "hefur verið fjarlægt"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Hakaðu við þennan reit ef athugasemdin er óviðeigandi. Skilaboðin „Þessi athugasemd hefur verið fjarlægð“ birtist í staðinn."
+
+#: models.py:80
+msgid "comments"
+msgstr "athugasemdir"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Athugasemdin var send inn af innskráðum notanda og því er ekki hægt að breyta nafninu."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Athugasemdin var send inn af innskráðum notanda og því er ekki hægt að breyta netfanginu."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s sendi inn %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flagga"
+
+#: models.py:180
+msgid "date"
+msgstr "dagsetning"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "flagg athugasemdar"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "flögg athugasemdar"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Samþykkja athugasemd"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Gera þessa athugasemd sýnilega?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Samþykkja"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Takk fyrir að samþykkja"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Takk fyrir að gefa þér tíma til að bæta gæði umræðunnar á síðunni."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Fjarlægja athugasemd"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Eyða þessari athugasemd?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Fjarlægja"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Takk fyrir að fjarlægja"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flagga athugasemd"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Flagga þesa athugasemd?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flagg"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Takk fyrir að flagga"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Birta"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Skoða"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Takk fyrir að senda athugasemd"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Takk fyrir athugasemdina"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Skoða athugasemd"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Birta athugasemd"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "eða breyta"
diff --git a/app/lib/django_comments/locale/it/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..9f5d2cc
--- /dev/null
+++ b/app/lib/django_comments/locale/it/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/it/LC_MESSAGES/django.po b/app/lib/django_comments/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e795f60
--- /dev/null
+++ b/app/lib/django_comments/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# C8E <C8E@miron.it>, 2015
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Nicola Larosa <transifex@teknico.net>, 2011
+# palmux <palmux@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-04-02 23:52+0000\n"
+"Last-Translator: palmux <palmux@gmail.com>\n"
+"Language-Team: Italian (http://www.transifex.com/django/django-contrib-comments/language/it/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Contenuto"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadati"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "segnalato"
+msgstr[1] "segnalati"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Segnala i commenti selezionati"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "approvato"
+msgstr[1] "approvati"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Approva i commenti selezionati"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminato"
+msgstr[1] "eliminati"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Elimina i commenti selezionati"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "Ad 1 commento è stata applicata con successo l'azione %(action)s."
+msgstr[1] "A %(count)s commenti è stata applicata con successo l'azione %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "commenti su %(site_name)s "
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Commenti più recenti su %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nome"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Indirizzo email"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Commento"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Modera i termini: la parola %s non è ammessa qui."
+msgstr[1] "Modera i termini: le parole %s non sono ammesse qui."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "e"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Se inserisci qualcosa in questo campo, il tuo commento verrà considerato spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "content type"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID dell'oggetto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "utente"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nome utente"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "indirizzo email utente"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL dell'utente"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "commento"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/ora di inserimento"
+
+#: models.py:63
+msgid "IP address"
+msgstr "indirizzo IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "è pubblico"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Deseleziona questa casella per far sparire del tutto il commento dal sito."
+
+#: models.py:67
+msgid "is removed"
+msgstr "è eliminato"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Seleziona la casella se il commento è inappropriato. Verrà sostituito dal messaggio \"Questo commento è stato rimosso\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "commenti"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Questo commento è stato inserito da un utente autenticato e quindi il nome non è modificabile."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Questo commento è stato inserito da un utente autenticato e quindi l'email non è modificabile."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Inserito da %(user)s il %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "Segnala"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "segnalazione commento"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "segnalazioni commenti"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nuovo commento pubblicato su \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Approva un commento"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Sicuro di voler pubblicare questo commento?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Approva"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Grazie per aver approvato"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Grazie per aver speso tempo a migliorare la qualità della discussione sul nostro sito"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Elimina un commento"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Sicuro di voler eliminare questo commento?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Elimina"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Grazie per aver eliminato"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Segnala questo commento"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Sicuro di voler segnalare questo commento?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Segnala"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Grazie per aver segnalato"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Pubblica"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Anteprima"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Grazie per aver commentato"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Grazie per il tuo commento"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Mostra l'anteprima del tuo commento"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Correggi l'errore qui sotto"
+msgstr[1] "Correggi gli errori qui sotto"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Pubblica il tuo commento"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "o fai dei cambiamenti"
diff --git a/app/lib/django_comments/locale/ja/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ja/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..454636c
--- /dev/null
+++ b/app/lib/django_comments/locale/ja/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ja/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ja/LC_MESSAGES/django.po
new file mode 100644
index 0000000..416c449
--- /dev/null
+++ b/app/lib/django_comments/locale/ja/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# xiu1 <d84ea@hotmail.co.jp>, 2015-2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-06-25 14:42+0000\n"
+"Last-Translator: xiu1 <d84ea@hotmail.co.jp>\n"
+"Language-Team: Japanese (http://www.transifex.com/django/django-contrib-comments/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "内容"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "メタデータ"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "にフラグが付きました"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "選択したコメントにフラグを付ける"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "は承認されました"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "選択したコメントを承認する"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "は削除されました"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "コメントを削除する"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s個のコメント%(action)s。"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s のコメント"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s の最新コメント"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "名前"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "メールアドレス"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "コメント"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "言葉使いに気を付けて! %s という言葉は使えません。"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "と"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "このフィールドに入力するとコメントはスパム扱いされます"
+
+#: models.py:23
+msgid "content type"
+msgstr "コンテンツタイプ"
+
+#: models.py:25
+msgid "object ID"
+msgstr "オブジェクト ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "ユーザー"
+
+#: models.py:55
+msgid "user's name"
+msgstr "名前"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "メールアドレス"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "コメント"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "コメント投稿日時"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP アドレス"
+
+#: models.py:64
+msgid "is public"
+msgstr "は公開中です"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "手っ取り早くコメントをサイトから消すにはここのチェックを外してください。"
+
+#: models.py:67
+msgid "is removed"
+msgstr "は削除されました"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "コメントが不適切な場合はチェックを入れてください。「コメントは削除されました」と表示されるようになります。"
+
+#: models.py:80
+msgid "comments"
+msgstr "コメント"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "このコメントは認証済みユーザーによって投稿されたため、名前は読み取り専用です。"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "このコメントは認証済みユーザーによって投稿されたため、メールアドレスは読み取り専用です。"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s が %(date)s に投稿\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "フラグ"
+
+#: models.py:180
+msgid "date"
+msgstr "フラグを付けた日時"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "コメントフラグ"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "コメントフラグ"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] 新しいコメントを \"%(object)s\" に投稿しました"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "コメントを承認する"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "本当にこのコメントを承認しますか?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "承認"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "ご利用ありがとうございました!"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "当サイトの品質向上にご協力いただきありがとうございました"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "コメントを削除する"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "本当にこのコメントを削除しますか?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "削除"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "ご利用ありがとうございました!"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "このコメントにフラグを付ける"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "本当にこのコメントにフラグを付けますか?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "フラグを付ける"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "ご利用ありがとうございました!"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "投稿"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "プレビュー"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "コメントしてくれてありがとうございました"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "コメントありがとうございました"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "コメントのプレビュー"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "下記のエラーを修正してください。"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "コメントを投稿"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "さらに編集"
diff --git a/app/lib/django_comments/locale/ka/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ka/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a9e2a36
--- /dev/null
+++ b/app/lib/django_comments/locale/ka/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ka/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ka/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e202ef5
--- /dev/null
+++ b/app/lib/django_comments/locale/ka/LC_MESSAGES/django.po
@@ -0,0 +1,286 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# avsd05 <avsd05@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Georgian (http://www.transifex.com/django/django-contrib-comments/language/ka/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ka\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "კონტენტი"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "მეტა-მონაცემები"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "დროშა"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "არჩეულ კომენტარებზე დროშის დასმა"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "დამტკიცებულია"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "არჩეული კომენტარების დამტკიცება"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "წაშლილია"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "არჩეული კომენტარების წაშლა"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "წარმატებით %(action)s %(count)s კომენტარი"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s-ის კომენტარები"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "ბოლო კომენტარები %(site_name)s-ზე"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ელ. ფოსტის მისამართი"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "კომენტარი"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "დაიცავით წესრიგი! სიტყვა \"%s\" აქ დაუშვებელია."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "და"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "თუ თვენ შეიყვანთ რამეს ამ ველში, თქვენი კომენტარი სპამად აღიქმება"
+
+#: models.py:23
+msgid "content type"
+msgstr "კონტენტის ტიპი"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ობიექტის ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "მომხმარებელი"
+
+#: models.py:55
+msgid "user's name"
+msgstr "მომხმარებლის სახელი"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "მომხმარებლის ელ. ფოსტა"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "მომხმარებლის URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "კომენტარი"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "გაგზავნის თარიღი და დრო"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-მისამართი"
+
+#: models.py:64
+msgid "is public"
+msgstr "საყოველთაოა"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "გადანიშნეთ ეს დროშა, რათა კომენტარი რეალურად გაქრეს საიტიდან."
+
+#: models.py:67
+msgid "is removed"
+msgstr "წაშლილია"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "მონიშნეთ ეს დროშა, თუ კომენტარი შეუსაბამოა. მის ნაცვლად გაჩნდება შეტყობინება: \"კომენტარი წაშლილია\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "კომენტარები"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "ეს კომენტარი გამოგზავნილია აუდენტიფიცირებული მომხმარებლის მიერ, და ამიტომ სახელის შეცვლა შეუძლებელია."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "ეს კომენტარი გამოგზავნილია აუდენტიფიცირებული მომხმარებლის მიერ, და ამიტომ ელ. ფოსტის შეცვლა შეუძლებელია."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "გამოგზავნილია %(user)s-ს მიერ, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "დროშა"
+
+#: models.py:180
+msgid "date"
+msgstr "თარიღი"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "კომენტარის დროშა"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "კომენტარის დროშები"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "კომენტარის დადასტურება"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "ნამდვილად გსურთ ამ კომენტარის გამოქვეყნება?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "დასტური"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "გმადლობთ, კომენტარის დადასტურებისათვის"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "გმადლობთ, რომ დრო დახარჯეთ ჩვენს საიტზე დისკუსიის ხარისხის გასაუმჯობესებლად"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "წავშალოთ კომენტარები"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "ნამდვილად გსურთ ამ კომენტარის წაშლა?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "წაშლა"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "გმადლობთ, წაშლისათვის"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "კომენტარის მარკირება"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "ნამდვილად გსურთ ამ კომენტარის მარკირება?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "დროშა"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "გმადლობთ, მარკირებისათვის"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "გაგზავნა"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "წინასწარი ნახვა"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "გმადლობთ კომენტარისთვის"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "გმადლობთ თქვენი კომენტარისათვის"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "კომენტარის წინასწარი ნახვა"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "კომენტარის გაგზავნა"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ან შეიტანეთ ცვლილებები"
diff --git a/app/lib/django_comments/locale/kk/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/kk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..1d4705b
--- /dev/null
+++ b/app/lib/django_comments/locale/kk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/kk/LC_MESSAGES/django.po b/app/lib/django_comments/locale/kk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a4cc527
--- /dev/null
+++ b/app/lib/django_comments/locale/kk/LC_MESSAGES/django.po
@@ -0,0 +1,286 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# yun_man_ger <germanilyin@gmail.com>, 2011
+# Zhazira <zhazira.mt@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Kazakh (http://www.transifex.com/django/django-contrib-comments/language/kk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: kk\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Мазмұн"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Метадата"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "белгіленген"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Таңдалған коментарийлерді белгілеу"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "Расталған"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Таңдалған аңғартпаларды бекіту"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "өшірілген"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Таңдалған аңғартпаларды өшіру"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "one: 1 аңғартпа ойдағыдай %(action)s.\nother: %(count)s аңғартпа ойдағыдай %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s аңғартпалары"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Соңғы %(site_name)s аңғартпалары"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Email адрес"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Аңғартпа"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Байқап сөйлеңіз! Бұл жерде %s сөзіне тыйым салынған."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "және"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Егер сіз бұл жолаққа қандай да бір нарсені енгізсеңіз, сіздің коментариіңіз спам ретінде белгіленеді."
+
+#: models.py:23
+msgid "content type"
+msgstr "мазмұн түрі"
+
+#: models.py:25
+msgid "object ID"
+msgstr "нысан ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "пайдаланушы"
+
+#: models.py:55
+msgid "user's name"
+msgstr "пайдаланушының есімі"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "пайдаланушының email адресі"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "пайдаланушының URLі"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "аңғартпа"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "жіберілген күні/уықыты"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP адрес"
+
+#: models.py:64
+msgid "is public"
+msgstr "ашық"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Анғартпаның сайттан жоғалуы үшін құсбелгіні алып тастаңыз."
+
+#: models.py:67
+msgid "is removed"
+msgstr "өшірілген"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Анғартпа дұрыс болмаса құсбелгігі қойыңыз. Орнына \"Бұл анғартпа өшірілді\" деген хабар көрінеді."
+
+#: models.py:80
+msgid "comments"
+msgstr "анғартпалар"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Бұл аңғартпа пайдаланушы орналастырған үшін аты өзгертілмейді."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Бұл аңғартпа пайдаланушы орналастырған үшін email өзгертілмейді."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s %(date)s орналастырды\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "жалауша"
+
+#: models.py:180
+msgid "date"
+msgstr "мерзім"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "Аңғартпа жалаушасы"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "Аңғартпа жалаушалары"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Аңғартпаны мақұлда"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Бұл аңғартпаны ашық қылуға сенімдісіз бе?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Мақұлдау"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Мақұлдау үшін рахмет"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Торпабымыздағы"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Аңғартпаны өшір"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Аңғартпаны өшіруге сенімдісіз бе?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Өшір"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Өшіргеніңіз үшін рахмет"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Бұл аңғартпаға жалауша қой"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Бұл аңғартпаға жалауша қоюға сенімдісіз бе?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Жалауша"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Жалауша қойғаныңыз үшін рахмет"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Орналастыру"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Алдын ала қарап алу"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Аңғартпаңыз үшін рахмет"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Аңғартпаңыз үшін рахмет"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Аңғартпаны алдын ала қарап алу"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Аңғартпаңызды орналастырыңыз"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "немесе өзгертіңіз"
diff --git a/app/lib/django_comments/locale/km/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/km/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..60983e7
--- /dev/null
+++ b/app/lib/django_comments/locale/km/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/km/LC_MESSAGES/django.po b/app/lib/django_comments/locale/km/LC_MESSAGES/django.po
new file mode 100644
index 0000000..18ea6a2
--- /dev/null
+++ b/app/lib/django_comments/locale/km/LC_MESSAGES/django.po
@@ -0,0 +1,285 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Khmer (http://www.transifex.com/django/django-contrib-comments/language/km/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: km\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "អត្ថបទ"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr "អាស័យដ្ឋានគេហទំព័រ(URL)"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "ផ្សេងៗ"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "និង"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "ប្រភេទអត្ថន័យ"
+
+#: models.py:25
+msgid "object ID"
+msgstr "លេខ​សំគាល់​កម្មវិធី"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "សមាជិក"
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "មតិ​យោបល់"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "កាល​បរិច្ឆេទនៃ​ការ​សរសេរ​​"
+
+#: models.py:63
+msgid "IP address"
+msgstr "លេខ IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "ផ្សព្វផ្សាយ​ជាសធារណៈ"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "ត្រូវ​បាន​លប់​ចេញ"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "សូម​ចុច​ជ្រើសរើស​យក​ប្រអប់​នេះ​ ប្រសិន​បើ​មតិ​យោបល់​មិនសមរម្យ។ ឃ្លា \" មតិ​យោបល់​នេះ​ត្រូវបាន​គេលប់​\" នឹងត្រូវ​បង្ហាញ​ជំនួស​វិញ។"
+
+#: models.py:80
+msgid "comments"
+msgstr "មតិ​យោបល់"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "សរសេរ​ដោយ %(user)s នៅថ្ងៃ​ %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "​កាលបរិច្ឆេទ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/kn/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/kn/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..593126e
--- /dev/null
+++ b/app/lib/django_comments/locale/kn/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/kn/LC_MESSAGES/django.po b/app/lib/django_comments/locale/kn/LC_MESSAGES/django.po
new file mode 100644
index 0000000..59617d2
--- /dev/null
+++ b/app/lib/django_comments/locale/kn/LC_MESSAGES/django.po
@@ -0,0 +1,286 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# karthikbgl <karthikbgl@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Kannada (http://www.transifex.com/django/django-contrib-comments/language/kn/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: kn\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "ವಿಷಯ"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "ಅಭಿಪ್ರಾಯ"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ಮತ್ತು"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "ಒಳವಿಷಯದ ಬಗೆ"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ವಸ್ತುವಿನ ಐಡಿ"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "ಬಳಕೆದಾರ"
+
+#: models.py:55
+msgid "user's name"
+msgstr "ಬಳಕೆದಾರನ ಹೆಸರು"
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "ಟಿಪ್ಪಣಿ"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "ಸಲ್ಲಿಸಿದ ದಿನಾಂಕ/ಸಮಯ"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP ವಿಳಾಸ"
+
+#: models.py:64
+msgid "is public"
+msgstr "ಸಾರ್ವಜನಿಕವಾಗಿದೆ"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "ತೆಗೆದು ಹಾಕಲಾಗಿದೆ"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "ಟಿಪ್ಪಣಿ ಅನುಚಿತವಾಗಿ ಇದ್ದಲ್ಲಿ ಈ ಚೌಕದಲ್ಲಿ ಗುರುತು ಮಾಡಿ. ಅದರ ಬದಲಾಗಿ \"ಈ ಟಿಪ್ಪಣಿ ತೆಗೆದುಹಾಕಲಾಗಿದೆ\" ಎಂಬ ಸಂದೇಶವನ್ನು ತೋರಿಸಲಾಗುವುದು."
+
+#: models.py:80
+msgid "comments"
+msgstr "ಟಿಪ್ಪಣಿಗಳು"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "ಸಲ್ಲಿಸಿದವರು %(user)s ರವರು %(date)s\n\n ದಿನ/ಸಮಯಕ್ಕೆ %(comment)s\n\nhttp://%(domain)s%(url)s ಸಲ್ಲಿಸಿದ್ದು"
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "ದಿನಾಂಕ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "ಅಭಿಪ್ರಾಯ ಒಪ್ಪಿಗೆ"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "ಒಪ್ಪಿಗೆ"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/ko/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ko/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..727eedb
--- /dev/null
+++ b/app/lib/django_comments/locale/ko/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ko/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ko/LC_MESSAGES/django.po
new file mode 100644
index 0000000..3b14d2b
--- /dev/null
+++ b/app/lib/django_comments/locale/ko/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Le Tartuffe <magno79@gmail.com>, 2016
+# Jiyoon, Ha <cryptography@konkuk.ac.kr>, 2016
+# Yeonsu Bak <yeonsubak@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-09-15 16:44+0000\n"
+"Last-Translator: Jiyoon, Ha <cryptography@konkuk.ac.kr>\n"
+"Language-Team: Korean (http://www.transifex.com/django/django-contrib-comments/language/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "내용"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "메타데이터"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "플래그되었습니다"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "선택된 코멘트에 플래그 달기"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "승인되었습니다"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "선택된 코멘트 승인하기"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "삭제되었습니다"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "선택된 코멘트 삭제"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s개의 코멘트가 성공적으로 %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s 의 코멘트"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s 의 사용자 비밀번호가 초기화 되었습니다."
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "이름"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "이메일 주소"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "코멘트"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "비속어/욕설입니다. %s (은)는 사용하실 수 없습니다."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "또한"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "이 필드에 무엇이라도 입력하면 코멘트는 스팸으로 처리될 것입니다."
+
+#: models.py:23
+msgid "content type"
+msgstr "콘텐츠 타입"
+
+#: models.py:25
+msgid "object ID"
+msgstr "오브젝트 ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "사용자"
+
+#: models.py:55
+msgid "user's name"
+msgstr "사용자명"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "사용자 이메일 주소"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "사용자 URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "코멘트"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "날짜/시간 확인"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP 주소"
+
+#: models.py:64
+msgid "is public"
+msgstr "공개합니다."
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "이 사이트에서 코멘트가 나타나지 않게 하려면 체크 해제하세요."
+
+#: models.py:67
+msgid "is removed"
+msgstr "삭제합니다."
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "코멘트가 부적절한 경우 체크하세요. \"코멘트가 삭제되었습니다.\" 메시지가 표시됩니다."
+
+#: models.py:80
+msgid "comments"
+msgstr "코멘트(들)"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "이 코멘트는 등록된 사용자가 작성하였으므로 읽기 전용입니다."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "이 코멘트는 등록된 사용자가 작성하였으므로 읽기 전용입니다."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s (이)가 %(date)s 등록\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "플래그"
+
+#: models.py:180
+msgid "date"
+msgstr "날짜"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "코멘트 플래그"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "코멘트 플래그"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] \"%(object)s\"에 새로운 코멘트가 게시되었습니다."
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "코멘트 승인"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "정말로 이 코멘트를 공개하시겠습니까?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "승인"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "승인해주셔서 고맙습니다."
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "우리 사이트의 토론에 기여해주셔서 감사합니다."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "코멘트 삭제"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "정말로 이 코멘트를 삭제하시겠습니까?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "삭제하기"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "삭제해주셔서 고맙습니다."
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "코멘트에 플래그 달기"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "정말로 플래그를 다시겠습니까?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "플래그를 답니다."
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "플래그를 달아주셔서 고맙습니다."
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "작성하기"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "미리보기"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "코멘트 작성 완료"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "코멘트를 달아주셔서 고맙습니다."
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "코멘트 미리보기"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "아래의 오류들을 고쳐주시기 바랍니다."
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "코멘트 작성하기"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "또는 변경하기"
diff --git a/app/lib/django_comments/locale/lt/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/lt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..02ea954
--- /dev/null
+++ b/app/lib/django_comments/locale/lt/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/lt/LC_MESSAGES/django.po b/app/lib/django_comments/locale/lt/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ab45c2b
--- /dev/null
+++ b/app/lib/django_comments/locale/lt/LC_MESSAGES/django.po
@@ -0,0 +1,305 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# lauris <lauris@runbox.com>, 2011
+# Matas Dailyda <matas@dailyda.com>, 2015-2016
+# Simonas Kazlauskas <simonas@kazlauskas.me>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-05-23 12:11+0000\n"
+"Last-Translator: Matas Dailyda <matas@dailyda.com>\n"
+"Language-Team: Lithuanian (http://www.transifex.com/django/django-contrib-comments/language/lt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: lt\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Turinys"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meta-duomenys"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "pažymėtas"
+msgstr[1] "pažymėti"
+msgstr[2] "pažymėti"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Žymėti pasirinktus komentarus"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "patvirtintas"
+msgstr[1] "patvirtinti"
+msgstr[2] "patvirtinti"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Patvirtinti pasirinktus komentarus"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "pašalintas"
+msgstr[1] "pašalinti"
+msgstr[2] "pašalinti"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Pašalinti pasirinktus įrašus"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentaras buvo sėkmingai %(action)s."
+msgstr[1] "%(count)s komentarai buvo sėkmingai %(action)s."
+msgstr[2] "%(count)s komentarai buvo sėkmingai %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s komentarai"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Paskutiniai %(site_name)s komentarai"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Vardas"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "El. pašto adresas"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Nuoroda"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentaras"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Žiūrėk ką kalbi! Žodis %s yra čia uždraustas."
+msgstr[1] "Žiūrėk ką kalbi! Žodžiai %s yra čia uždrausti."
+msgstr[2] "Žiūrėk ką kalbi! Žodžiai %s yra čia uždrausti."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ir"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Jei ką nors įrašysite į šį laukelį, jūsų komentaras bus laikomas brukalu"
+
+#: models.py:23
+msgid "content type"
+msgstr "turinio tipas"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekto ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "vartotojas"
+
+#: models.py:55
+msgid "user's name"
+msgstr "vartotojo vardas"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "vartotojo el. pašto adresas"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "vartotojo nuoroda"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentaras"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "įvesta data/laikas"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresas"
+
+#: models.py:64
+msgid "is public"
+msgstr "viešas"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Nuimkite šį žymejimą, kad komentaras būtų panaikintas."
+
+#: models.py:67
+msgid "is removed"
+msgstr "pašalintas"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Pažymėkite šį laukelį, jei komentaras netinkamas. \"Šis komentaras ištrintas\" bus rodoma."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentarai"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Šis komentaras buvo paskelbtas neprisijungusio vartotojo, todel vardas yra neredaguojamas."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Šis komentaras buvo paskelbtas neprisijungusio vartotojo, todel el. pašto adresas yra neredaguojamas."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Paskelbta %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "žymė"
+
+#: models.py:180
+msgid "date"
+msgstr "Data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "Komentaro žymė"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "Komentaro žymės"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nauji komentarai įvesti \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Patvirtinti komentarą"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Tikrai publikuoti šį komentarą?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Patvirtinti"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Ačiū už patvirtinimą"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Ačiū, kad radote laiko pagerinti diskusijų kokybę mūsų svetainėje"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Pašalinti komentarą"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Tikrai ištrinti šį komentarą?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Pašalinti"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Ačiū už pašalinimą"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Pažymėti šį komentarą"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Tikrai žymėti šį komentarą?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Žymė"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Ačiū už žymėjimą"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Siųsti"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Peržiūra"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Ačiū už komentarą"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Ačiū už jūsų komentarą"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Peržiūrėti savo komentarą"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Prašome ištaisyti žemiau esančią klaidą"
+msgstr[1] "Prašome ištaisyti žemiau esančias klaidas"
+msgstr[2] "Prašome ištaisyti žemiau esančias klaidas"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publikuoti komentarą"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "arba keisti"
diff --git a/app/lib/django_comments/locale/lv/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/lv/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..003fbb7
--- /dev/null
+++ b/app/lib/django_comments/locale/lv/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/lv/LC_MESSAGES/django.po b/app/lib/django_comments/locale/lv/LC_MESSAGES/django.po
new file mode 100644
index 0000000..59e3f88
--- /dev/null
+++ b/app/lib/django_comments/locale/lv/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Latvian (http://www.transifex.com/django/django-contrib-comments/language/lv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: lv\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Saturs"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadati"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "atzīmēts"
+msgstr[1] "atzīmēti"
+msgstr[2] "atzīmēts"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Atzīmēt izvēlētos komentārus"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "apstiprināts"
+msgstr[1] "apstiprināti"
+msgstr[2] "apstiprināti"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Apstiprināt izvēlētos komentārus"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "dzēsts"
+msgstr[1] "dzēsti"
+msgstr[2] "dzēsts"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Dzēst izvēlētos komentārus"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentārs tika veiksmīgi %(action)s."
+msgstr[1] "%(count)s komentāri tika veiksmīgi %(action)s."
+msgstr[2] "%(count)s komentāru tika veiksmīgi %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s komentāri"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Pēdējie komentāri lapā %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-pasta adrese"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentārs"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Lūdzam ievērot pieklājību! Vārds %s šeit nav atļauts."
+msgstr[1] "Lūdzam ievērot pieklājību! Vārdi %s šeit nav atļauti."
+msgstr[2] "Lūdzam ievērot pieklājību! Vārdi %s šeit nav atļauti."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "un"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ja aizpildīsiet šo lauku, tad komentārs tiks uzskatīts par spamu"
+
+#: models.py:23
+msgid "content type"
+msgstr "satura tips"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekta ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "lietotājs"
+
+#: models.py:55
+msgid "user's name"
+msgstr "lietotāja vārds"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "lietotāja e-pasta adrese"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "lietotāja URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentārs"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "nosūtīšanas datums/laiks"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adrese"
+
+#: models.py:64
+msgid "is public"
+msgstr "publisks"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Noņemiet ķeksi, lai komentārs neparādītos lapā."
+
+#: models.py:67
+msgid "is removed"
+msgstr "dzēsts"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Atķeksējiet, ja komentārs ir neatbilstošs (nepieklājīgs). Tā vietā tiks rādīts paziņojums \"Šis komentārs ir izdzēsts\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentāri"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Šo komentāru rakstīja autentificēts lietotājs, tāpēc vārds ir tikai lasīšanas režīmā."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Šo komentāru rakstīja autentificēts lietotājs, tāpēc e-pasts ir tikai lasīšanas režīmā."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Pievienojis %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "atzīmēt"
+
+#: models.py:180
+msgid "date"
+msgstr "datums"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "komentāra atzīmējums"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "komentāra atzīmējumi"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Apstiprināt komentāru"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Patiešām padarīt šo komentāru publisku?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Apstiprināt"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Paldies par apstiprināšanu"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Paldies par laika veltīšanu mūsu lapas diskusiju kvalitātes uzlabošanai"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Dzēst komentāru"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Patiešām dzēst šo komentāru?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Dzēst"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Paldies par dzēšanu"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Atzīmēt šo komentāru"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Patiešām atzīmēt šo komentāru?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Atzīmējums"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Paldies par komentāra atzīmēšanu"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Iesūtīt"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Priekšskats"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Paldies par komentēšanu"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Paldies par Jūsu komentāru"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Pirmsskatīt komentāru"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Iesūtīt komentāru"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "vai veikt izmaiņas"
diff --git a/app/lib/django_comments/locale/mk/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/mk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a69a582
--- /dev/null
+++ b/app/lib/django_comments/locale/mk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/mk/LC_MESSAGES/django.po b/app/lib/django_comments/locale/mk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a721d2e
--- /dev/null
+++ b/app/lib/django_comments/locale/mk/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Macedonian (http://www.transifex.com/django/django-contrib-comments/language/mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Содржина"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Метаподатоци"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "обележан"
+msgstr[1] "обележани"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Обележи го одбраните коментари"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "одобрен"
+msgstr[1] "одобрени"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Одобри ги одбраните коментари"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "отстранет"
+msgstr[1] "отстранети"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Отстрани ги избраните коментари"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s коментар беше успешно %(action)s."
+msgstr[1] "%(count)s коментари беа успешно %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "коментари за %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Последни коментари за %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Е-пошта"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Коментар"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Внимавајте на јазикот. Тука не е дозволен зборот %s."
+msgstr[1] "Внимавајте на јазикот. Тука не се дозволени зборовите %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "и"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ако внесете нешто во ова поле вашиот коментар ќе биде означен како спам"
+
+#: models.py:23
+msgid "content type"
+msgstr "тип на содржина"
+
+#: models.py:25
+msgid "object ID"
+msgstr "object ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "корисник"
+
+#: models.py:55
+msgid "user's name"
+msgstr "името на корисникот"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "е-пошта на корисникот"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "веб страна на корсникот"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "коментар"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "датум/време пријавен"
+
+#: models.py:63
+msgid "IP address"
+msgstr "ИП адреса"
+
+#: models.py:64
+msgid "is public"
+msgstr "е јавен"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Одштиклирајте го ова за да направите коментаров да исчезне од овој сајт."
+
+#: models.py:67
+msgid "is removed"
+msgstr "е отстранет"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Штиклирајте го ова поле ако коментарот не е пригоден. Наместо него пораката „Овој коментар беше отстранет“ ќе биде прикажана."
+
+#: models.py:80
+msgid "comments"
+msgstr "коментари"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Овој коментар бил пратен од автентициран корисник и затоа името е заштитено од промена."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Овој коментар бил пратен од автентициран корисник и затоа е-пошта е заштитена од промена."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Напишан од %(user)s на %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "обележи"
+
+#: models.py:180
+msgid "date"
+msgstr "датум"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "обележје за коментар"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "обележја за коментари"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Одобри коментар"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Навистина ли сакате овој коментар да биде објавен?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Одобри"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Ви благодариме што одобривте"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Ви благодариме што допринесовте да се подобри квалитетот на дискусиите на нашиот сајт"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Избриши коментар"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Навистина ли сакате да го отстраните овој коментар?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Отстрани"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Ви благодариме што отстранивте"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Обележи го овој коментар"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Навистина ли сакате да го обележите овој коментар?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Обележи"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Ви благодариме што обележавте"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Објави"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Преглед"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Ви благодариме за коментарот"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Ви благодариме за коментарот"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Прегледајте го вашиот коментар"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Објавете го вашиот коментар"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "или направете измени"
diff --git a/app/lib/django_comments/locale/ml/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ml/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e7f184e
--- /dev/null
+++ b/app/lib/django_comments/locale/ml/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ml/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ml/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7dafb12
--- /dev/null
+++ b/app/lib/django_comments/locale/ml/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Rajeesh Nair <rajeeshrnair@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Malayalam (http://www.transifex.com/django/django-contrib-comments/language/ml/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ml\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "ഉള്ളടക്കം"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "മെറ്റാ-ഡാറ്റ"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "അടയാളപ്പെടുത്തി"
+msgstr[1] "അടയാളപ്പെടുത്തി"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ അടയാളപ്പെടുത്തുക"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "അംഗീകരിച്ചു"
+msgstr[1] "അംഗീകരിച്ചു"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ അംഗീകരിക്കുക"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "നീക്കം ചെയ്തു"
+msgstr[1] "നീക്കം ചെയ്തു"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ നീക്കം ചെയ്യുക"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 അഭിപ്രായം വിജയകരമായി %(action)s."
+msgstr[1] "%(count)s അഭിപ്രായങ്ങള്‍ വിജയകരമായി %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s അഭിപ്രായങ്ങള്‍"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s ലെ ഏറ്റവും പുതിയ അഭിപ്രായങ്ങള്‍"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ഇ-മെയില്‍ വിലാസം"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL(വെബ്-വിലാസം)"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "അഭിപ്രായം"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "ശ്ശ്ശ്! %s എന്ന വാക്ക് ഇവിടെ അനുവദനീയമല്ല."
+msgstr[1] "ശ്ശ്ശ്! %s എന്നീ വാക്കുകള്‍ ഇവിടെ അനുവദനീയമല്ല."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ഉം"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "ഈ കള്ളിയില്‍ എന്തെങ്കിലും രേഖപ്പെടുത്തിയാല്‍ നിങ്ങളുടെ അഭിപ്രായം സ്പാം ആയി കണക്കാക്കും"
+
+#: models.py:23
+msgid "content type"
+msgstr "ഏതു തരം ഉള്ളടക്കം"
+
+#: models.py:25
+msgid "object ID"
+msgstr "വസ്തുവിന്റെ ID (തിരിച്ചറിയല്‍ സംഖ്യ)"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "യൂസര്‍ (ഉപയോക്താവ്)"
+
+#: models.py:55
+msgid "user's name"
+msgstr "യൂസറുടെ പേര്"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "യൂസറുടെ ഇ-മെയില്‍ വിലാസം"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "യൂസറുടെ URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "അഭിപ്രായം"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "സമര്‍പ്പിച്ച തീയതി/സമയം"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP വിലാസം"
+
+#: models.py:64
+msgid "is public"
+msgstr "പരസ്യമാണ്"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "അഭിപ്രായം സൈറ്റില്‍ നിന്നും ഫലപ്രദമായി നീക്കം ചെയ്യാന്‍ ഈ ബോക്സിലെ ടിക് ഒഴിവാക്കുക."
+
+#: models.py:67
+msgid "is removed"
+msgstr "നീക്കം ചെയ്തു."
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "അഭിപ്രായം അനുചിതമെങ്കില്‍ ഈ ബോക്സ് ടിക് ചെയ്യുക. \"ഈ അഭിപ്രായം നീക്കം ചെയ്തു \" എന്ന സന്ദേശം ആയിരിക്കും പകരം കാണുക."
+
+#: models.py:80
+msgid "comments"
+msgstr "അഭിപ്രായങ്ങള്‍"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "ഈ അഭിപ്രായം ഒരു അംഗീകൃത യൂസര്‍ രേഖപ്പെടുത്തിയതാണ്. അതിനാല്‍ പേര് വായിക്കാന്‍ മാത്രം."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "ഈ അഭിപ്രായം ഒരു അംഗീകൃത യൂസര്‍ രേഖപ്പെടുത്തിയതാണ്. അതിനാല്‍ ഇ-മെയില്‍ വിലാസം വായിക്കാന്‍ മാത്രം."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(date)sന് %(user)s രേഖപ്പെടുത്തിയത്:\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "അടയാളം"
+
+#: models.py:180
+msgid "date"
+msgstr "തീയതി"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "അഭിപ്രായ അടയാളം"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "അഭിപ്രായ അടയാളങ്ങള്‍"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "അഭിപ്രായം അംഗീകരിക്കൂ"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "ശരിക്കും ഈ അഭിപ്രായം പരസ്യമാക്കണോ?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "അംഗീകരിക്കൂ"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "അംഗീകരിച്ചതിനു നന്ദി"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "നമ്മുടെ സൈറ്റിലെ ചര്‍ച്ചകളുടെ നിലവാരം ഉയര്‍ത്താന്‍ സമയം ചെലവഴിച്ചതിനു നന്ദി."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "അഭിപ്രായം നീക്കം ചെയ്യൂ"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "ശരിക്കും ഈ അഭിപ്രായം നീക്കം ചെയ്യണോ?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "നീക്കം ചെയ്യുക"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "നീക്കം ചെയ്തതിനു നന്ദി"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "ഈ അഭിപ്രായം അടയാളപ്പെടുത്തൂ"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "ഈ അഭിപ്രായം ശരിക്കും അടയാളപ്പെടുത്തണോ?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "അടയാളം"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "അടയാളപ്പെടുത്തിയതിനു നന്ദി"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "രേഖപ്പെടുത്തൂ"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "അവലോകനം"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "അഭിപ്രായം രേഖപ്പെടുത്തിയതിനു നന്ദി"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "അഭിപ്രായത്തിനു നന്ദി"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "അഭിപ്രായം അവലോകനം ചെയ്യുക"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "അഭിപ്രായം രേഖപ്പെടുത്തുക"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "അല്ലെങ്കില്‍ മാറ്റം വരുത്തുക."
diff --git a/app/lib/django_comments/locale/mn/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/mn/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a16211e
--- /dev/null
+++ b/app/lib/django_comments/locale/mn/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/mn/LC_MESSAGES/django.po b/app/lib/django_comments/locale/mn/LC_MESSAGES/django.po
new file mode 100644
index 0000000..ffbb68f
--- /dev/null
+++ b/app/lib/django_comments/locale/mn/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Анхбаяр Анхаа <l.ankhbayar@gmail.com>, 2011
+# Баясгалан Цэвлээ <bayasaa_7672@yahoo.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-07-21 13:48+0000\n"
+"Last-Translator: Баясгалан Цэвлээ <bayasaa_7672@yahoo.com>\n"
+"Language-Team: Mongolian (http://www.transifex.com/django/django-contrib-comments/language/mn/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mn\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Агуулга"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Мета өгөгдөл"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "тэгдэглсэн"
+msgstr[1] "тэгдэглсэн"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Сонгосон сэтгэгдэлүүдийг тэмдэглэ"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "зөвшөөрөх"
+msgstr[1] "зөвшөөрөх"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Сонгосон сэтгэгдэлүүдийг зөвшөөрөх"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "устгасан"
+msgstr[1] "устгасан"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Сонгосон сэтгэгдэлүүдийг утсгах"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s сэтгэгдэлийг амжилттай %(action)s."
+msgstr[1] "%(count)s сэтгэгдэлүүдийг амжилттай %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s сэтгэгдэлүүд"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr " %(site_name)s сүүлийн сэтгэгдэлүүд"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Нэр"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Цахим шуудангийн хаяг"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Сэтгэгдэл"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Соёлтой байна уу! %s гэсэн үг оруулах хориотой."
+msgstr[1] "Соёлтой байна уу! %s гэсэн үгүүд оруулах хориотой."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ба"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Та энэ хэсэгт ямар нэг зүйл оруулбал таний сэтгэгдэлийг спам гэж үзэх болно."
+
+#: models.py:23
+msgid "content type"
+msgstr "агуулгын төрөл"
+
+#: models.py:25
+msgid "object ID"
+msgstr "объектийн ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "хэрэглэгч "
+
+#: models.py:55
+msgid "user's name"
+msgstr "хэрэглэгчийн нэр"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "хэрэглэгчийн цахим шуудангийн хаяг"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "хэрэглэгчийн URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "сэтгэгдэл"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "оруулсан огноо/цаг"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP хаяг"
+
+#: models.py:64
+msgid "is public"
+msgstr "нийтийн"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Сайтаас санал сэтгэгдлийг байнга устгахын тулд энэ хайрцагны өмнөх чагтыг авна уу."
+
+#: models.py:67
+msgid "is removed"
+msgstr "устлаа"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Ирсэн санал сэтгэгдэл зүй зохисгүй бол энэ хайрцгийг чагтла. Ингэснээр тухайн санал сэтгэгдлийн оронд \"Энэ санал сэтгэгдлийг устгалаа\" гэсэн бичиг гарч ирнэ."
+
+#: models.py:80
+msgid "comments"
+msgstr "сэтгэгдэлүүд"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Энэ санал сэтгэгдлийг баталгаажсан хэрэглэгч оруулсан учир зөвхөн нэрийг нь харж болно."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Энэ санал сэтгэгдлийг баталгаажсан хэрэглэгч оруулсан учир зөвхөн цахим шууданг нь харж болно."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(date)s-д %(user)s дараах санал сэтгэгдлийг үлдээжээ\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "тэмдэг"
+
+#: models.py:180
+msgid "date"
+msgstr "огноо"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "Тайлбарын тэмдэг"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "тайлбарын тэмдэгүүд"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Сэтгэгдэл зөвшөөрөх"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Үнэхээр энэ сэтгэгдэлийн нийтийн болгох гэж байна у?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Зөвшөөрөх"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Зөвшөөрсөнд баяраллаа"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Манай сайтанд цаг гаргаж хэлэлцүүлэгийг сонирхолтой болгосонд баяраллаа."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Үнэхээр энэ сэтгэдэлийг"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Үнхээр энэ сэтгэдэлийг устгах уу?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Устгах"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Устгасанд баяраллаа"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Энэ сэтгэгдэлийг тэмлэглэ"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Үнэхээр энэ сэтгэдэлийг тэмдэглэх үү?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Тэмдэг"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Тэмдэглсэнд баяраллаа"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Бичлэг"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Урьдчилан харах"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Сэтгэгдэл үлдээсэнд баяраллаа"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Сэтгэгдэл үлдээсэн таньд баяраллаа"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Өөрийн сэтгэгдэлээ урьдчилан харах"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Доорх алдааг залруулна уу!"
+msgstr[1] "Доорх алдаануудыг залруулна уу!"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Сэтгэгдэл оруулах"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "эсвэл засвар хийх"
diff --git a/app/lib/django_comments/locale/nb/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/nb/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b8afee9
--- /dev/null
+++ b/app/lib/django_comments/locale/nb/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/nb/LC_MESSAGES/django.po b/app/lib/django_comments/locale/nb/LC_MESSAGES/django.po
new file mode 100644
index 0000000..2ebb7eb
--- /dev/null
+++ b/app/lib/django_comments/locale/nb/LC_MESSAGES/django.po
@@ -0,0 +1,294 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Jon <jon@funkbit.no>, 2015
+# <jonklo@gmail.com>, 2012
+# Sigurd Gartmann <sigurdga-transifex@sigurdga.no>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-09-14 08:36+0000\n"
+"Last-Translator: Jon <jon@funkbit.no>\n"
+"Language-Team: Norwegian Bokmål (http://www.transifex.com/django/django-contrib-comments/language/nb/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nb\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Innhold"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flagget"
+msgstr[1] "flagget"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Flagg valgte kommentarer"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "godkjent"
+msgstr[1] "godkjent"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Tillat valgte kommentarer"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "fjernet"
+msgstr[1] "fjernet"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Fjern valgte kommentarer"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 kommentar ble %(action)s."
+msgstr[1] "%(count)s kommentarer ble %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s kommentarer"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Siste kommentarer fra %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Navn"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-postadresse"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Nettadresse"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pass munnen din! Ordet %s er ikke tillatt her."
+msgstr[1] "Pass munnen din! Ordene %s er ikke tillatt her."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "og"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Hvis du oppgir noe i dette feltet, vil kommentaren bli behandlet som spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "innholdstype"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekt-ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "bruker"
+
+#: models.py:55
+msgid "user's name"
+msgstr "brukerens navn"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "brukerens e-postadresse"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "brukerens nettadresse"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dato/tid for innsendelse"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adresse"
+
+#: models.py:64
+msgid "is public"
+msgstr "er tilgjengelig for alle"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Fjern avhuking av denne boksen for å fjerne kommentaren fra siden."
+
+#: models.py:67
+msgid "is removed"
+msgstr "er fjernet"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Huk av denne hvis kommentaren er upassende. Meldingen «Denne kommentaren har blitt fjernet» vil bli vist i stedet."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentarer"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Denne kommentaren er skrevet av en innlogget bruker og navnet kan derfor ikke endres."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Denne kommentaren er skrevet av en innlogget bruker og e-postadressen kan derfor ikke endres."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Skrevet av %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flagg"
+
+#: models.py:180
+msgid "date"
+msgstr "dato"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentarflagg"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommentarflagg"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Tillat en kommentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Gjør denne kommentaren offentlig?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Godkjenn"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Takk for godkjennelse"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Takk for at du tok deg tid til å forbedre kvaliteten på diskusjonen på siden vår"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Fjern en kommentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Fjern denne kommentaren?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Fjern"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Takk for fjerningen"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flagg denne kommentaren"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Flagg denne kommentaren?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flagg"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Takk for flagging"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publiser"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Forhåndsvisning"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Takk for kommentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Takk for din kommentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Forhåndsvis kommentaren din"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Vennligst korriger feilen under"
+msgstr[1] "Vennligst korriger feilene under"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publiser din kommentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "eller gjør endringer"
diff --git a/app/lib/django_comments/locale/ne/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ne/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b82ed75
--- /dev/null
+++ b/app/lib/django_comments/locale/ne/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ne/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ne/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4a6f0df
--- /dev/null
+++ b/app/lib/django_comments/locale/ne/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Paras Nath Chaudhary <opnchaudhary@gmail.com>, 2012
+# Sagar Chalise <chalisesagar@gmail.com>, 2011
+# Surit Aryal <surit_people@hotmail.com>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Nepali (http://www.transifex.com/django/django-contrib-comments/language/ne/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ne\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "विषय"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "मेटाडाटा"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "स्वीकृत"
+msgstr[1] "स्वीकृत"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "चुनिएको प्रतिकृया स्वीकार गर्नुहोस ।"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "हटाइएको छ"
+msgstr[1] "हटाइएको छ"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "चुनिएको प्रतिकृया हटाउनुहोस ।"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s प्रतिकृया"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)sका ताजा प्रतिकृया"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ई-मेल ठेगाना"
+
+#: forms.py:98
+msgid "URL"
+msgstr ""
+
+#: forms.py:99
+msgid "Comment"
+msgstr "प्रतिकृया"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "र"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "याहा केहि पनि नलेख्नु होला । याहा केहि लेखियेमा तेसला"
+
+#: models.py:23
+msgid "content type"
+msgstr "विषयको ढाँचा"
+
+#: models.py:25
+msgid "object ID"
+msgstr ""
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "प्रयोगकर्ता "
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "प्रतिकृया"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr ""
+
+#: models.py:63
+msgid "IP address"
+msgstr ""
+
+#: models.py:64
+msgid "is public"
+msgstr ""
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "हटाइएको छ"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr "प्रतिकृया"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "मिति"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "प्रतिकृया स्वीकार गर्नुहोस"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "स्वीकार गर्नुहोस"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "स्वीकार गरेकोमा धन्यवाद"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "प्रतिकृया हटाउनुहोस"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "के साच्चै प्रतिकृया हटाउनुहुन्छ ?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "हटाउनुहोस"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "हटाएकोमा धन्यवाद"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "लेख "
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "पूर्व समीक्षा"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "प्रतिकृयाको लागि धन्यवाद"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "तपाइको प्रतिकृयाको लागि धन्यवाद"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "तपाइको प्रतिकृयाको पूर्व समीक्षा"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "तपाइको प्रतिकृयाको पठाउनुहोस"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "अथवा फेरबदल गर्नुहोस"
diff --git a/app/lib/django_comments/locale/nl/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..3b05375
--- /dev/null
+++ b/app/lib/django_comments/locale/nl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/nl/LC_MESSAGES/django.po b/app/lib/django_comments/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..fddd817
--- /dev/null
+++ b/app/lib/django_comments/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# go2people <admin@go2people.nl>, 2011
+# Evelijn Saaltink <evelijnsaaltink@gmail.com>, 2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Tino de Bruijn <tinodb@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-10-12 18:03+0000\n"
+"Last-Translator: Evelijn Saaltink <evelijnsaaltink@gmail.com>\n"
+"Language-Team: Dutch (http://www.transifex.com/django/django-contrib-comments/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Inhoud"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "gemarkeerd"
+msgstr[1] "gemarkeerd"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Markeer geselecteerde commentaren"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "goedgekeurd"
+msgstr[1] "goedgekeurd"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Keur geselecteerde commentaren goed"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "verwijderd"
+msgstr[1] "verwijderd"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Verwijder geselecteerde commentaren"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] " %(count)s opmerking werd met succes %(action)s ."
+msgstr[1] " %(count)s opmerkingen werden met succes %(action)s ."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s opmerkingen"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Laatste opmerkingen op %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Naam"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mailadres"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Opmerking"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pas op uw taalgebruik! Gebruik van %s niet toegestaan."
+msgstr[1] "Pas op uw taalgebruik! Gebruik van de woorden %s is niet toegestaan."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "en"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Indien u hier iets invult dan wordt uw opmerking behandeld als spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "inhoudstype"
+
+#: models.py:25
+msgid "object ID"
+msgstr "object-ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "gebruiker"
+
+#: models.py:55
+msgid "user's name"
+msgstr "naam gebruiker"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "e-mailadres gebruiker"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL gebruiker"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "opmerking"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum/tijd toegevoegd"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adres"
+
+#: models.py:64
+msgid "is public"
+msgstr "is openbaar"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Vink dit vakje uit zodat de opmerking niet meer zichtbaar is op de site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "is verwijderd"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Vink dit vak aan indien de opmerking niet gepast is. Een \"Dit commentaar is verwijderd\" bericht wordt dan getoond."
+
+#: models.py:80
+msgid "comments"
+msgstr "opmerkingen"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Deze opmerking is gepost door een ingelogde gebruiker en daardoor is de naam niet aan te passen."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Deze opmerking is gepost door een ingelogde gebruiker en daardoor is het e-mailadres niet aan te passen."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Geplaatst door %(user)s op %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "vlag"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "opmerking vlag"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "opmerking vlaggen"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nieuwe opmerking geplaatst op \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Een opmerking toestaan"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Weet u zeker dat u deze opmerking openbaar wilt maken?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Goedkeuren"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Bedankt voor het goedkeuren"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Bedankt dat u de tijd heeft genomen om de kwaliteit van de discussie op onze site te verbeteren"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Een opmerking verwijderen"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Weet u zeker dat u deze opmerking wilt verwijderen?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Verwijderen"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Bedankt voor het verwijderen"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Vlag deze opmerking"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Weet u zeker dat u deze opmerking wilt vlaggen?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Vlag"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Bedankt voor het vlaggen"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Bericht"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Voorbeeld"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Bedankt voor uw opmerking"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Bedankt voor uw opmerking"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Toon voorbeeld van uw opmerking"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Please correct the error below"
+msgstr[1] "Herstel de fouten hieronder"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Plaats uw opmerking"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "of maak aanpassingen"
diff --git a/app/lib/django_comments/locale/nn/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/nn/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6ec92b2
--- /dev/null
+++ b/app/lib/django_comments/locale/nn/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/nn/LC_MESSAGES/django.po b/app/lib/django_comments/locale/nn/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7cbcb2c
--- /dev/null
+++ b/app/lib/django_comments/locale/nn/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Norwegian Nynorsk (http://www.transifex.com/django/django-contrib-comments/language/nn/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nn\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Innhald"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flagga"
+msgstr[1] "flagga"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Flagg valde kommentarar"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "Godkjend"
+msgstr[1] "Godkjende"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Tillat valde kommentarar"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "er fjerna"
+msgstr[1] "er fjerna"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Fjern valdte kommentarar"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "Ein kommentar vart %(action)s."
+msgstr[1] "%(count)s kommentarar vart %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s - kommentarar"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Siste kommentarar frå %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-postadresse"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Nettadresse"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pass munnen din! Ordet %s er ikkje lovleg her."
+msgstr[1] "Pass munnen din! Orda %s er ikkje lovlege her."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "og"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Dersom du oppgjev noko i dette feltet, vil kommentaren bli behandla som spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "innhaldstype"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objekt-ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "brukar"
+
+#: models.py:55
+msgid "user's name"
+msgstr "brukaren sitt namn"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "brukaren si e-postadresse"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "brukaren si nettadresse"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dato/tid for innsending"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adresse"
+
+#: models.py:64
+msgid "is public"
+msgstr "er tilgjengeleg for alle"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Avmerk denne boksen for å fjerne kommentaren frå sida."
+
+#: models.py:67
+msgid "is removed"
+msgstr "er fjerna"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Kryss av denne dersom kommentaren er upassande. Meldinga \"Denne kommentaren har blitt fjerna\" vil bli vist i staden."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentarar"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Denne kommentaren er skriven av ein innlogga brukar og namnnet kan difor ikkje endrast."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Denne kommentaren er skriven av ein innlogga brukar og e-postadressa kan derfor ikkje endrast."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Skrive av %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flagg"
+
+#: models.py:180
+msgid "date"
+msgstr "dato"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentarflagg"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommentarflagg"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Tillat ein kommentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Gjere denne kommentaren offentleg?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Godkjenn"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Takk for godkjenning"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Takk for at du tok deg tid til å forbetre kvaliteten på diskusjonen på sida vår"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Fjern ein kommentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Fjerne denne kommentaren?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Fjern"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Takk for fjerninga"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flagg denne kommentaren"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Flagg denne kommentaren?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flagg"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Takk for flagging"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publiser"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Førehandsvising"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Takk for kommentaren"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Takk for kommentaren din"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Førehandsvis kommentaren din"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publiser kommentaren din"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "eller gjer endringar"
diff --git a/app/lib/django_comments/locale/pa/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/pa/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..ee7b180
--- /dev/null
+++ b/app/lib/django_comments/locale/pa/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/pa/LC_MESSAGES/django.po b/app/lib/django_comments/locale/pa/LC_MESSAGES/django.po
new file mode 100644
index 0000000..3c1e550
--- /dev/null
+++ b/app/lib/django_comments/locale/pa/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Panjabi (Punjabi) (http://www.transifex.com/django/django-contrib-comments/language/pa/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pa\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "ਸਮੱਗਰੀ"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "ਮੇਟਾਡਾਟਾ"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "ਚੁਣੀਆਂ ਟਿੱਪਣੀਆਂ ਫਲੈਗ ਕਰੋ"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "ਮਨਜ਼ੂਰ ਹੈ"
+msgstr[1] "ਮਨਜ਼ੂਰ ਹਨ"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "ਚੁਣੀਆਂ ਟਿੱਪਣੀਆਂ ਮਨਜ਼ੂਰ"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "ਹਟਾਈ"
+msgstr[1] "ਹਟਾਈਆਂ"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "ਚੁਣੀਆਂ ਟਿੱਪਣੀਆਂ ਹਟਾਓ"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "੧ ਟਿੱਪਣੀ ਠੀਕ ਤਰ੍ਹਾਂ %(action)s ਗਈ।"
+msgstr[1] "%(count)s ਟਿੱਪਣੀਆਂ ਠੀਕ ਤਰ੍ਹਾਂ %(action)s ਗਈਆਂ।"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s ਟਿੱਪਣੀਆਂ"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s ਉੱਤੇ ਤਾਜ਼ਾ ਟਿੱਪਣੀਆਂ"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ਈਮੇਲ ਐਡਰੈੱਸ"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "ਟਿੱਪਣੀ"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ਅਤੇ"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "ਸਮੱਗਰੀ ਕਿਸਮ"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ਆਬਜੈਕਟ ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "ਯੂਜ਼ਰ"
+
+#: models.py:55
+msgid "user's name"
+msgstr "ਯੂਜ਼ਰ ਦਾ ਨਾਂ"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "ਯੂਜ਼ਰ ਦਾ ਈਮੇਲ ਐਡਰੈੱਸ"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "ਯੂਜ਼ਰ ਦਾ URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "ਟਿੱਪਣੀ"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "ਭੇਜਣ ਮਿਤੀ/ਸਮਾਂ"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP ਐਡਰੈੱਸ"
+
+#: models.py:64
+msgid "is public"
+msgstr "ਪਬਲਿਕ ਹੈ"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "ਹਟਾਇਆ ਗਿਆ"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr "ਟਿੱਪਣੀਆਂ"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s ਵਲੋਂ %(date)s ਨੂੰ ਪੋਸਟ ਕੀਤੀ\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "ਫਲੈਗ"
+
+#: models.py:180
+msgid "date"
+msgstr "ਮਿਤੀ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "ਟਿੱਪਣੀ ਫਲੈਗ"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "ਟਿੱਪਣੀ ਫਲੈਗ"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "ਟਿੱਪਣੀ ਮਨਜ਼ੂਰ ਕਰੋ"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "ਇਹ ਟਿੱਪਣੀ ਪਬਲਿਕ ਬਣਾਉਣੀ ਹੈ?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "ਮਨਜ਼ੂਰ"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "ਮਨਜ਼ੂਰ ਕਰਨ ਲਈ ਧੰਨਵਾਦ ਜੀ"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "ਟਿੱਪਣੀ ਹਟਾਓ"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "ਇਹ ਟਿੱਪਣੀ ਹਟਾਉਣੀ ਹੈ?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "ਹਟਾਓ"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "ਹਟਾਉਣ ਲਈ ਧੰਨਵਾਦ"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "ਇਹ ਟਿੱਪਣੀ ਲਈ ਫਲੈਗ"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "ਇਹ ਟਿੱਪਣੀ ਲਈ ਫਲੈਗ ਲਾਉਣਾ ਹੈ?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "ਫਲੈਗ"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "ਫਲੈਗ ਲਗਾਉਣ ਲਈ ਧੰਨਵਾਦ"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "ਪੋਸਟ ਕਰੋ"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "ਝਲਕ"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "ਟਿੱਪਣੀ ਦੇਣ ਲਈ ਧੰਨਵਾਦ"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "ਤੁਹਾਡੀ ਟਿੱਪਣੀ ਲਈ ਤੁਹਾਡਾ ਧੰਨਵਾਦ"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "ਆਪਣੀ ਟਿੱਪਣੀ ਦੀ ਝਲਕ ਵੇਖੋ"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "ਆਪਣੀ ਟਿੱਪਣੀ ਪੋਸਟ ਕਰੋ"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ਜਾਂ ਬਦਲਾਅ ਕਰੋ"
diff --git a/app/lib/django_comments/locale/pl/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..23f4900
--- /dev/null
+++ b/app/lib/django_comments/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/pl/LC_MESSAGES/django.po b/app/lib/django_comments/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..cc8c3d3
--- /dev/null
+++ b/app/lib/django_comments/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,304 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Adrian Antończyk <linkedin@antonczyk.it>, 2015
+# Jannis Leidel <jannis@leidel.info>, 2011
+# m_aciek <maciej.olko@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-04-17 23:17+0000\n"
+"Last-Translator: m_aciek <maciej.olko@gmail.com>\n"
+"Language-Team: Polish (http://www.transifex.com/django/django-contrib-comments/language/pl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Zawartość"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadane"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "oflagowany"
+msgstr[1] "oflagowane"
+msgstr[2] "oflagowanych"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Oflaguj wybrane komentarze"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "zaakceptowany"
+msgstr[1] "zaakceptowane"
+msgstr[2] "zaakceptowanych"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Zaakceptuj wybrane komentarze"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "usunięty"
+msgstr[1] "usunięte"
+msgstr[2] "usuniętych"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Usuń wybrane komentarze"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 komentarz został %(action)s"
+msgstr[1] "%(count)s komentarze zostały %(action)s"
+msgstr[2] "%(count)s komentarzy zostało %(action)s"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "komentarze na %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Ostatnie komentarze na %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nazwa"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adres e-mail"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentarz"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Nie wolno przeklinać! Słowo %s nie jest dozwolone."
+msgstr[1] "Nie wolno przeklinać! Słowa %s nie są dozwolone."
+msgstr[2] "Nie wolno przeklinać! Słowa %s nie są dozwolone."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "i"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Jeżeli wpiszesz cokolwiek w to pole, Twój komentarz zostanie uznany za spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "typ zawartości"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID obiektu"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "użytkownik"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nazwa użytkownika"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adres e-mail użytkownika"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL użytkownika"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentarz"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/czas dodania"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Adres IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "publicznie dostępny"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Odznacz to pole, aby komentarz nie był wyświetlany w serwisie."
+
+#: models.py:67
+msgid "is removed"
+msgstr "usunięty"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Zaznacz to pole jeżeli komentarz jest nieodpowiedni. Wyświetlony zostanie tekst \"Ten komentarz został usunięty\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentarze"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ten komentarz został dodany przez zalogowanego użytkownika i dlatego nazwa jest tylko do odczytu."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ten komentarz został dodany przez zalogowanego użytkownika i dlatego adres e-mail jest tylko do odczytu."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Dodane przez %(user)s dnia %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flaga"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "flaga dla komentarza"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "flagi dla komentarzy"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Dodany komentarz do „%(object)s”"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Zaakceptuj komentarz"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Czy ten komentarz na pewno ma być publiczny?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Zaakceptuj"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Dziękujemy za zaakceptowanie"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Dziękujemy za poświęcenie czasu na podniesienie jakości dyskusji w naszym serwisie"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Usuń komentarz"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Czy na pewno usunąć ten komentarz?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Usuń"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Dziękujemy za usunięcie"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Oflaguj ten komentarz"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Czy na pewno oflagować ten komentarz?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flaga"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Dziękujemy za oflagowanie"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Zapisz"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Podgląd"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Dziękujemy za dodanie komentarza"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Dziękujemy za komentarz"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Podgląd komentarza"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Proszę poprawić poniższy błąd."
+msgstr[1] "Proszę poprawić poniższe błędy."
+msgstr[2] "Proszę poprawić poniższe błędy."
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Zapisz swój komentarz"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "lub wprowadź jakieś zmiany"
diff --git a/app/lib/django_comments/locale/pt/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/pt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..099a1c9
--- /dev/null
+++ b/app/lib/django_comments/locale/pt/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/pt/LC_MESSAGES/django.po b/app/lib/django_comments/locale/pt/LC_MESSAGES/django.po
new file mode 100644
index 0000000..dde005f
--- /dev/null
+++ b/app/lib/django_comments/locale/pt/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Nuno Mariz <nmariz@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Portuguese (http://www.transifex.com/django/django-contrib-comments/language/pt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Conteúdo"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcado"
+msgstr[1] "marcados"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar os comentários selecionados"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprovado"
+msgstr[1] "aprovados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprovar os comentários selecionados"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "removido"
+msgstr[1] "removidos"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Remover os comentários selecionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 comentário foi %(action)s com sucesso."
+msgstr[1] "%(count)s comentários foram %(action)s com sucesso."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "comentários em %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentários em %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Endereço de e-mail"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentário"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Atenção à linguagem! A palavra %s não é permitida aqui."
+msgstr[1] "Atenção à linguagem! As palavras %s não são permitidas aqui."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "e"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Se introduzir alguma coisa neste campo o seu comentário irá ser tratado como spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de conteúdo"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID do objeto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "utilizador"
+
+#: models.py:55
+msgid "user's name"
+msgstr "o nome do utilizador"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "endereço de e-mail do utilizador"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL do utilizador"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentário"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hora de submissão"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Endereço IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "é público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Não selecione esta caixa para que o comentário desapareça do site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "foi removido"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Selecione esta opção se o comentário não é apropriado. Uma mensagem \"Este comentário foi removido\" será mostrada no seu lugar."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentários"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentário foi colocado por um utilizador autenticado, portanto o nome é apenas de leitura."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentário foi colocado por um utilizador autenticado, portanto o e-mail é apenas de leitura."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Colocado pelo utilizador %(user)s em %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marcar"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marca de comentário"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcas de comentários"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprovar um comentário"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Tem a certeza que deseja tornar este comentário público?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprovar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Obrigado pela aprovação"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Obrigado por dedicar o seu tempo para melhorar a qualidade de discussão no nosso site"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Remover um comentário"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Tem a certeza que deseja remover este comentário?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Remover"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Obrigado pela remoção"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar este comentário"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Tem a certeza que deseja marcar este comentário?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Obrigado por marcar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publicar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Prever"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Obrigado por comentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Obrigado pelo seu comentário"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Pré-visualizar comentário"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publique o seu comentário"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ou faça modificações"
diff --git a/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..2ab3fbd
--- /dev/null
+++ b/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.po b/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..d3368d7
--- /dev/null
+++ b/app/lib/django_comments/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Mairieli Wessel <mairieliw@alunos.utfpr.edu.br>, 2016
+# Patrick Biesdorf <patrickdorf@gmail.com>, 2015
+# Raysa Dutra, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-09-05 22:52+0000\n"
+"Last-Translator: FilipeCifali <cifali.filipe@gmail.com>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/django/django-contrib-comments/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Conteúdo"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meta-dados"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcado"
+msgstr[1] "marcados"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marcar comentários selecionados"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprovado"
+msgstr[1] "aprovados"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprovar comentários selecionados"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "removido"
+msgstr[1] "removidos"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Remover comentários selecionados"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 comentários foi %(action)s com sucesso."
+msgstr[1] "%(count)s comentários foram %(action)s com sucesso."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Comentários de %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Últimos comentários em %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nome"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Endereço de e-mail"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentário"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Olha sua boca! A palavra %s não é permitida aqui."
+msgstr[1] "Olha sua boca! As palavras %s não são permitidas aqui."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "e"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Se você inserir qualquer coisa neste campo, seu comentário será tratado como spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tipo de conteúdo"
+
+#: models.py:25
+msgid "object ID"
+msgstr "id do objeto"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "usuário"
+
+#: models.py:55
+msgid "user's name"
+msgstr "nome do usuário"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "endereço de e-mail do usuário"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL do usuário"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentário"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/hora de envio"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Endereço IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "é público"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Desmarque esta caixa para fazer o comentário desaparecer efetivamente deste site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "foi removido"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Selecione esta opção se o comentário é inapropriado. A mensagem \"Este comentário foi removido\" será mostrada no lugar."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentários"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Este comentário foi feito por um usuário autenticado e portanto seu nome é apenas para leitura."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Este comentário foi feito por um usuário autenticado e portanto seu e-mail é apenas para leitura."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Enviado por %(user)s em %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marcar"
+
+#: models.py:180
+msgid "date"
+msgstr "data"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marcar comentário"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcar comentários"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Novo comentário postado em \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprovar um comentário"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Realmente tornar este comentário público?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprovar"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Obrigado pela aprovação"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Obrigado por dedicar tempo para melhorar a qualidade de discussão de nosso site."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Remover um comentário"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Realmente remover este comentário?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Remover"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Obrigado pela remoção"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcar este comentário"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Realmente marcar este comentário?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marcar"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Obrigado pela marcação"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publicar"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Visualizar"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Obrigado por comentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Obrigado pelo seu comentário"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Visualizar seu comentário"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Por favor corrija os erros abaixo"
+msgstr[1] "Por favor corrija os erros abaixo"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publique seu comentário"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ou faça modificações"
diff --git a/app/lib/django_comments/locale/ro/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ro/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..db56e40
--- /dev/null
+++ b/app/lib/django_comments/locale/ro/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ro/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ro/LC_MESSAGES/django.po
new file mode 100644
index 0000000..9aa3af3
--- /dev/null
+++ b/app/lib/django_comments/locale/ro/LC_MESSAGES/django.po
@@ -0,0 +1,300 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Daniel Ursache-Dogariu <contact@danniel.net>, 2011
+# Denis Darii <denis.darii@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Razvan Stefanescu <razvan.stefanescu@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2016-01-07 11:08+0000\n"
+"Last-Translator: Razvan Stefanescu <razvan.stefanescu@gmail.com>\n"
+"Language-Team: Romanian (http://www.transifex.com/django/django-contrib-comments/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Conţinut"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadate"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "marcat"
+msgstr[1] "marcate"
+msgstr[2] "marcate"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Marchează comentariile selectate"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "aprobat"
+msgstr[1] "aprobate"
+msgstr[2] "aprobate"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Aprobă comentariile selectate"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "eliminat"
+msgstr[1] "eliminate"
+msgstr[2] "eliminate"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Elimină comentariile selectate"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s comentariu a fost %(action)s cu succes."
+msgstr[1] "%(count)s comentarii au fost %(action)s cu succes."
+msgstr[2] "%(count)s de comentarii au fost %(action)s cu succes."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Comentariile de la %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Ultimele comentarii la %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Nume"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adresă e-mail"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Comentariu"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Atenție la limbajul folosit! Cuvântul %s nu este permis aici."
+msgstr[1] "Atenție la limbajul folosit! Cuvintele %s nu sunt permise aici."
+msgstr[2] "Atenție la limbajul folosit! Cuvintele %s nu sunt permise aici."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "și"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Dacă introduceți ceva în acest câmp, comentariul dumneavoastră va fi considerat ca spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "tip conţinut"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID obiect"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "utilizator"
+
+#: models.py:55
+msgid "user's name"
+msgstr "numele utilizatorului"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adresa e-mail a utilizatorului"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL-ul utilizatorului"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "comentariu"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/ora creării"
+
+#: models.py:63
+msgid "IP address"
+msgstr "adresă ip"
+
+#: models.py:64
+msgid "is public"
+msgstr "este public"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Debifaţi această casetă pentru a face comentariul să dispară de pe site."
+
+#: models.py:67
+msgid "is removed"
+msgstr "este șters"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Bifați această casuța dacă acest comentariu nu este adecvat. Un mesaj de tipul \"Acest comentariu a fost șters\" va fi afișat în schimb."
+
+#: models.py:80
+msgid "comments"
+msgstr "comentarii"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Acest comentariu a fost scris de către un utilizator autentificat, astfel numele poate fi doar citit."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Acest comentariu a fost scris de către un utilizator autentificat, astfel adresa e-mail poate fi doar citită."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Scris de %(user)s la %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "marcaj"
+
+#: models.py:180
+msgid "date"
+msgstr "dată"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "marcaj comentariu"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "marcaje comentarii"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Aprobă un comentariu"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Chiar doriți să faceți public acest comentariu?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Aprobă"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Mulțumiri pentru aprobare"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Vă mulţumim pentru a timpul acordat spre a îmbunătăți calitatea discuțiilor de pe situl nostru"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Elimină un comentariu"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Chiar doriți să eliminați acest comentariu?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Elimină"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Mulțumiri pentru eliminare"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Marcați acest comentariu"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Chiar doriți să marcați acest comentariu?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Marchează"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Mulțumiri pentru marcare"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Publică"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Previzualizează"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Mulțumiri pentru comentariu"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Vă mulțumim pentru comentariu"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Previzualizați-vă comentariul"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Corectați eroarea de mai jos"
+msgstr[1] "Corectați erorile de mai jos"
+msgstr[2] "Corectați erorile de mai jos"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Publicați-vă comentariul"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "sau faceți modificări"
diff --git a/app/lib/django_comments/locale/ru/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e14b7dc
--- /dev/null
+++ b/app/lib/django_comments/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ru/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..99ea065
--- /dev/null
+++ b/app/lib/django_comments/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,311 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Alexandr <alexromantsov@gmail.com>, 2016
+# Denis Darii <denis.darii@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Алексей Борискин <sun.void@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-04-27 02:25+0000\n"
+"Last-Translator: Alexandr <alexromantsov@gmail.com>\n"
+"Language-Team: Russian (http://www.transifex.com/django/django-contrib-comments/language/ru/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Содержание"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Метаданные"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "отмечен"
+msgstr[1] "отмечено"
+msgstr[2] "отмечено"
+msgstr[3] "отмечено"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Пометить выбранные комментарии"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "утверждён"
+msgstr[1] "утверждено"
+msgstr[2] "утверждено"
+msgstr[3] "утверждено"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Одобрить выбранные комментарии"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "удалён"
+msgstr[1] "удалено"
+msgstr[2] "удалено"
+msgstr[3] "удалено"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Удалить выбранные комментарии"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s комментарий был успешно %(action)s."
+msgstr[1] "%(count)s комментария было успешно %(action)s."
+msgstr[2] "%(count)s комментариев было успешно %(action)s."
+msgstr[3] "%(count)s комментариев было успешно %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Комментарии %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Последние комментарии на %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Имя"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Адрес электронной почты"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Комментарий"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Следите за собой! Слова %s не разрешены здесь."
+msgstr[1] "Следите за собой! Слова %s не разрешены здесь."
+msgstr[2] "Следите за собой! Слова %s не разрешены здесь."
+msgstr[3] "Следите за собой! Слова %s не разрешены здесь."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "и"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Если вы что-то введете в это поле, то ваш комментарий будет помечен как спам"
+
+#: models.py:23
+msgid "content type"
+msgstr "тип содержимого"
+
+#: models.py:25
+msgid "object ID"
+msgstr "идентификатор объекта"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "пользователь"
+
+#: models.py:55
+msgid "user's name"
+msgstr "имя пользователя"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "адрес электронной почты пользователя"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL пользователя"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "комментарий"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "дата и время добавления"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-адрес"
+
+#: models.py:64
+msgid "is public"
+msgstr "публичный"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Снимите выделение, чтобы убрать комментарий с сайта."
+
+#: models.py:67
+msgid "is removed"
+msgstr "удалён"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Отметьте, если комментарий нежелателен. Взамен будет показано сообщение \"Этот комментарий был удален\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "Комментарии"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Комментарий был добавлен зарегистрированным пользователем, поэтому имя пользователя доступно только для чтения."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Комментарий был добавлен зарегистрированным пользователем, поэтому адрес электронной почты доступен только для чтения."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Добавил %(user)s %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "отметка"
+
+#: models.py:180
+msgid "date"
+msgstr "дата"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "отметка комментария"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "отметки комментариев"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Новый комментарий опубликован на \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Одобрить комментарий"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Вы уверены, что хотите опубликовать этот комментарий?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Одобрить"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Спасибо, что одобрили комментарий"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Спасибо, что заботитесь о качестве общения на нашем сайте"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Удалить комментарий"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Вы уверены, что хотите удалить этот комментарий?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Удалить"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Спасибо за удаление"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Отметить этот комментарий"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Вы уверены, что хотите отметить этот комментарий?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Отметить"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Спасибо"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Опубликовать"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Предпросмотр"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Спасибо за комментарий"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Спасибо за ваш комментарий"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Предпросмотр вашего комментария"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Пожалуйста, исправьте ошибки ниже"
+msgstr[1] "Пожалуйста, исправьте ошибки ниже"
+msgstr[2] "Пожалуйста, исправьте ошибки ниже"
+msgstr[3] "Пожалуйста, исправьте ошибки ниже"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Опубликуйте ваш комментарий"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "или внесите изменения"
diff --git a/app/lib/django_comments/locale/sk/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..85946b6
--- /dev/null
+++ b/app/lib/django_comments/locale/sk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sk/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..d36da02
--- /dev/null
+++ b/app/lib/django_comments/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Slovak (http://www.transifex.com/django/django-contrib-comments/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Obsah"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metaúdaje"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "označený"
+msgstr[1] "označené"
+msgstr[2] "označených"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označiť vybraný komentár"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "schválený"
+msgstr[1] "schválené"
+msgstr[2] "schválených"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Schváliť vybraný komentár"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "odstránený"
+msgstr[1] "odstránené"
+msgstr[2] "odstránených"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Odstrániť vybrané komentáre"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 komentár bol úspešne %(action)s."
+msgstr[1] "%(count)s komentáre boli úspešne %(action)s."
+msgstr[2] "%(count)s komentárov bolo úspešne %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s komentáre"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Najnovšie komentáre na %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mail adresa"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentár"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Vyjadrujte sa slušne! Slovo %s tu nie je dovolené používať."
+msgstr[1] "Vyjadrujte sa slušne! Slová %s tu nie je dovolené používať."
+msgstr[2] "Vyjadrujte sa slušne! Slová %s tu nie je dovolené používať."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "a"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ak ste do tohoto poľa čokoľvek zadali, váš komentár bude považovaný za spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "typ obsahu"
+
+#: models.py:25
+msgid "object ID"
+msgstr "identifikátor objektu"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "používateľ"
+
+#: models.py:55
+msgid "user's name"
+msgstr "meno používateľa"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "e-mail adresa používateľa"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL používateľa"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentár"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "dátum a čas odoslania"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresa"
+
+#: models.py:64
+msgid "is public"
+msgstr "je verejný"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Ak chcete, aby komentár zmizol zo stránky, zrušte zaškrtnutie tohoto políčka."
+
+#: models.py:67
+msgid "is removed"
+msgstr "je odstránený"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Zaškrtnite toto pole, ak je komentár nevhodný. Namiesto neho sa zobrazí správa \"Tento komenár bol odstránený\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentáre"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Tento komentár je od autentifikovaného používateľa a preto je jeho meno len na čítanie."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Tento komentár je od autentifikovaného používateľa a preto je jeho e-mail len na čítanie."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Pridaný užívateľom %(user)s dňa %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "príznak"
+
+#: models.py:180
+msgid "date"
+msgstr "dátum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "komentárový príznak"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "komentárové príznaky"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Povoliť komentár"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Skutočne chcete zverejniť tento komentár?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Povoliť"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Ďakujeme za povolenie"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Ďakujeme za čas, ktorý ste venovali zvýšniu kvality diskusie na našej stránke"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Zmazať komentár"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Naozaj chcete zmazať tento komentár?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Odstrániť"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Ďakujeme za odstránenie"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označiť tento komentár"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Naozaj chcete označiť tento komentár?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Príznak"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Ďakujeme za označenie"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Poslať"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Náhľad"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Vďaka za komentár"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Ďakujeme za váš komentár"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Náhľad komentára"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Poslať váš komentár"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "alebo urobiť zmeny"
diff --git a/app/lib/django_comments/locale/sl/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..25f51e3
--- /dev/null
+++ b/app/lib/django_comments/locale/sl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sl/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..84febb4
--- /dev/null
+++ b/app/lib/django_comments/locale/sl/LC_MESSAGES/django.po
@@ -0,0 +1,309 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# zejn <zejn@kiberpipa.org>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-05-14 13:51+0000\n"
+"Last-Translator: zejn <zejn@kiberpipa.org>\n"
+"Language-Team: Slovenian (http://www.transifex.com/django/django-contrib-comments/language/sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sl\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Vsebina"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metapodatki"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "označenih"
+msgstr[1] "označen"
+msgstr[2] "označena"
+msgstr[3] "označeni"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označi izbrane komentarje"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "odobrenih"
+msgstr[1] "odobren"
+msgstr[2] "odobrena"
+msgstr[3] "odobreni"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Odobri izbrane opokomentarje"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "odstranjenih"
+msgstr[1] "odstranjen"
+msgstr[2] "odstranjena"
+msgstr[3] "odstranjeni"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Odstrani izbrane komentarje"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentarev je uspešno %(action)s."
+msgstr[1] "%(count)s komentar je uspešno %(action)s."
+msgstr[2] "%(count)s komentarja sta uspešno %(action)s."
+msgstr[3] "%(count)s komentarji so uspešno %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Komentarji %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Zadnji komentarji na %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Ime"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Elektronski naslov"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Naslov URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pazite na jezik! Besede %s tu niso dovoljene."
+msgstr[1] "Pazite na jezik! Beseda %s tu ni dovoljena."
+msgstr[2] "Pazite na jezik! Besedi %s tu nista dovoljeni."
+msgstr[3] "Pazite na jezik! Besede %s tu niso dovoljene."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "in"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Če v to polje vnesete kakršnokoli besedilo, bo vaš komentar označen kot neželen komentar."
+
+#: models.py:23
+msgid "content type"
+msgstr "vrsta vsebine"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID predmeta"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "uporabnik"
+
+#: models.py:55
+msgid "user's name"
+msgstr "ime uporabnika"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "elektronski naslov uporabnika"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "naslov URL uporabnika"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum in čas objave"
+
+#: models.py:63
+msgid "IP address"
+msgstr "naslov IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "je javno"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Odkljukajte izbor in komentar bo izginil s strani."
+
+#: models.py:67
+msgid "is removed"
+msgstr "je odstranjen"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Odkljukajte, če je komentarj neprimeren. Namesto komentarja bo prikazano obvestilo \"Komentar je bil odstranjen\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentarji"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Komentar je objavil prijavljen uporabnik, zato je ime na voljo le za branje."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Komentar je objavil prijavljen uporabnik, zato je elektronski naslov na voljo le za branje."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Objavljeno z računa %(user)s ob %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "označi"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "oznaka komentarja"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "oznake komentarja"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Nov objavljen komentar na \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Odobri komentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Ali ste prepričani, da želite ta komentar objaviti?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Odobri"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Hvala za odobritev"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Hvala, da ste si vzeli čas in pomagali izboljšati kakovost pogovorov na naši strani."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Odstrani komentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Ali res želite odstraniti ta komentar?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Odstrani"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Hvala, ker ste odstranili komentar."
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označi ta komentar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Ali res želite označiti ta komentar?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Označi"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Hvala, ker ste označili komentar"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Objavi"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Predogled"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Hvala za objavljanje komentarjev"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Hvala za vaš komentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Predogled komentarja"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Odpravite naslednjo napako"
+msgstr[1] "Odpravite naslednji napaki"
+msgstr[2] "Odpravite naslednje napake"
+msgstr[3] "Odpravite naslednje napake"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Objavite komentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ali naredite spremembe"
diff --git a/app/lib/django_comments/locale/sq/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sq/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..085c1d8
--- /dev/null
+++ b/app/lib/django_comments/locale/sq/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sq/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sq/LC_MESSAGES/django.po
new file mode 100644
index 0000000..b50e3b5
--- /dev/null
+++ b/app/lib/django_comments/locale/sq/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Besnik <besnik@programeshqip.org>, 2011
+# Besnik <besnik@programeshqip.org>, 2015-2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-10-11 21:34+0000\n"
+"Last-Translator: Besnik <besnik@programeshqip.org>\n"
+"Language-Team: Albanian (http://www.transifex.com/django/django-contrib-comments/language/sq/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sq\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Lëndë"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Tejtëdhëna"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "me shenjë"
+msgstr[1] "me shenjë"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Vëru shenjë komenteve të përzgjedhura"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "i miratuar"
+msgstr[1] "të miratuar"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Miratoji komentet e përzgjedhura"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "i hequr"
+msgstr[1] "të hequr"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Hiqi komentet e përzgjedhur"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 koment %(action)s me sukses."
+msgstr[1] "%(count)s komente %(action)s me sukses."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "komente te %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Komentet më të fundit te %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Emër"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Adresë email"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Koment"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Mbani gojën! Këtu nuk lejohet fjala %s."
+msgstr[1] "Mbani gojën! Këtu nuk lejohen fjalët %s."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr " dhe "
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Nëse fusni diçka në këtë fushë, komenti juaj do të trajtohet si i padëshiruar"
+
+#: models.py:23
+msgid "content type"
+msgstr "lloj lënde"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID objekti"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "përdorues"
+
+#: models.py:55
+msgid "user's name"
+msgstr "emri i përdoruesit"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "adresa email e përdoruesit"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL përdoruesi"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "koment"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "data/koha e parashtrimit"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Adresë IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "është publike"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Hiqjani shenjën kësaj kutize që ta bëni komentin të zhduket faktikisht prej sajtit."
+
+#: models.py:67
+msgid "is removed"
+msgstr "është hequr"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "I vini shenjë kësaj kutie, nëse komenti është i papërshtatshëm. Në vend të tij do të shfaqet një mesazh \"Ky koment është hequr\"."
+
+#: models.py:80
+msgid "comments"
+msgstr "komente"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ky koment u postua nga një përdorues i mirëfilltësuar, ndaj emri është nën atributin vetëm-lexim."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ky koment u postua nga një përdorues i mirëfilltësuar, ndaj email-i është nën atributin vetëm-lexim."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postuar nga %(user)s më %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "shenjë"
+
+#: models.py:180
+msgid "date"
+msgstr "datë"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "shenjë komenti"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "shenja komenti"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Koment i ri i postuar te \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Miratoni një koment"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Të bëhet vërtet publik ky koment?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Miratoje"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Faleminderit që e miratuat"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Faleminderit për kohën e harxhuar për përmirësimin e cilësisë së diskutimit në site-in tonë."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Hiqni një koment"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Të hiqet vërtet ky koment?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Hiqe"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Faleminderit që e hoqët"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "I vini shenjë këtij komenti"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "T'i vihet shenjë vërtet këtij komenti?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Vëri shenjë"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Faleminderit që i vutë shenjë"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Postoje"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Paraparje"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Faleminderit që komentuat"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Faleminderit për komentin tuaj"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Parashiheni komentin tuaj"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Ju lutem, ndreqni gabimin më poshtë"
+msgstr[1] "Ju lutem, ndreqni gabimet më poshtë"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Postojeni komentin tuaj"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ose bëni ndryshime"
diff --git a/app/lib/django_comments/locale/sr/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..7031045
--- /dev/null
+++ b/app/lib/django_comments/locale/sr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sr/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..07bcb58
--- /dev/null
+++ b/app/lib/django_comments/locale/sr/LC_MESSAGES/django.po
@@ -0,0 +1,298 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Janos Guljas <janos@resenje.org>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Serbian (http://www.transifex.com/django/django-contrib-comments/language/sr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sr\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Садржај"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Метаподаци"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "означен"
+msgstr[1] "означена"
+msgstr[2] "означена"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Означавање изабраних коментара"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "одобрен"
+msgstr[1] "одобрена"
+msgstr[2] "одобрена"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Одобрење изабраних коментара"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "избрисан"
+msgstr[1] "избрисана"
+msgstr[2] "избрисана"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Обриши изабране коментаре"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s коментар је успешно %(action)s."
+msgstr[1] "%(count)s коментара су успешно %(action)s."
+msgstr[2] "%(count)s коментара су успешно %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Коментари на сајту %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Скорији коментари на сајту %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Имејл адреса"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Коментари"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Пазите шта пишете! Реч %s није дозвољена овде."
+msgstr[1] "Пазите шта пишете! Речи %s нису дозвољене овде."
+msgstr[2] "Пазите шта пишете! Речи %s нису дозвољене овде."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "и"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ако ишта унесете у ово поље, Ваш коментар ће се сматрати спамом."
+
+#: models.py:23
+msgid "content type"
+msgstr "тип садржаја"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID објекта"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "корисник"
+
+#: models.py:55
+msgid "user's name"
+msgstr "корисниково име"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "корисникова имејл адреса"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "корисников URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "коментар"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "датум/време постављања"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP адреса"
+
+#: models.py:64
+msgid "is public"
+msgstr "јавно"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Деселектујте ово поље ако желите да порука фактички нестане са овог сајта."
+
+#: models.py:67
+msgid "is removed"
+msgstr "уклоњен"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Обележите ову кућицу ако је коментар неприкладан. Порука о уклањању ће бити приказана уместо коментара."
+
+#: models.py:80
+msgid "comments"
+msgstr "коментари"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Овај коментар је поставио пријављен корисник и зато је поље са именом закључано."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Овај коментар је поставио пријављен корисник и зато је поље са имејл адресом закључано."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Поставио %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "ознака"
+
+#: models.py:180
+msgid "date"
+msgstr "датум"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "ознака коментара"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "ознаке коментара"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Одобрење коментара"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Да ли заиста желите да означите овај коментар јавним?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Одобри"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Хвала на одобрењу!"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Хвала на учешћу у унапређењу квалитета дискусија на нашем сајту."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Обриши коментар"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Да ли заиста желите да обришете овај коментар?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Обриши"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Хвала што користите наш сајт!"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Означавање коментара"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Да ли заиста желите да означите овај коментар?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Означи"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Хвала што сте означили коментар."
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Постави"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Преглед"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Хвала на коментару"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Хвала што сте оставили свој коментар"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Прегледај коментар"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Постави коментар"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "или изврши измене"
diff --git a/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..92d7116
--- /dev/null
+++ b/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.po
new file mode 100644
index 0000000..460b8d2
--- /dev/null
+++ b/app/lib/django_comments/locale/sr_Latn/LC_MESSAGES/django.po
@@ -0,0 +1,298 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Janos Guljas <janos@resenje.org>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Serbian (Latin) (http://www.transifex.com/django/django-contrib-comments/language/sr@latin/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sr@latin\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Sadržaj"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metapodaci"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "označen"
+msgstr[1] "označena"
+msgstr[2] "označena"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Označavanje izabranih komentara"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "odobren"
+msgstr[1] "odobrena"
+msgstr[2] "odobrena"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Odobrenje izabranih komentara"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "izbrisan"
+msgstr[1] "izbrisana"
+msgstr[2] "izbrisana"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Obriši izabrane komentare"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s komentar je uspešno %(action)s."
+msgstr[1] "%(count)s komentara su uspešno %(action)s."
+msgstr[2] "%(count)s komentara su uspešno %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "Komentari na sajtu %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Skoriji komentari na sajtu %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Imejl adresa"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Komentari"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Pazite šta pišete! Reč %s nije dozvoljena ovde."
+msgstr[1] "Pazite šta pišete! Reči %s nisu dozvoljene ovde."
+msgstr[2] "Pazite šta pišete! Reči %s nisu dozvoljene ovde."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "i"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ako išta unesete u ovo polje, Vaš komentar će se smatrati spamom."
+
+#: models.py:23
+msgid "content type"
+msgstr "tip sadržaja"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID objekta"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "korisnik"
+
+#: models.py:55
+msgid "user's name"
+msgstr "korisnikovo ime"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "korisnikova imejl adresa"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "korisnikov URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "komentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "datum/vreme postavljanja"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresa"
+
+#: models.py:64
+msgid "is public"
+msgstr "javno"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Deselektujte ovo polje ako želite da poruka faktički nestane sa ovog sajta."
+
+#: models.py:67
+msgid "is removed"
+msgstr "uklonjen"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Obeležite ovu kućicu ako je komentar neprikladan. Poruka o uklanjanju će biti prikazana umesto komentara."
+
+#: models.py:80
+msgid "comments"
+msgstr "komentari"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imenom zaključano."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imejl adresom zaključano."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Postavio %(user)s, %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "oznaka"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "oznaka komentara"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "oznake komentara"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Odobrenje komentara"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Da li zaista želite da označite ovaj komentar javnim?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Odobri"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Hvala na odobrenju!"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Hvala na učešću u unapređenju kvaliteta diskusija na našem sajtu."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Obriši komentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Da li zaista želite da obrišete ovaj komentar?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Obriši"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Hvala što koristite naš sajt!"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Označavanje komentara"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Da li zaista želite da označite ovaj komentar?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Označi"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Hvala što ste označili komentar."
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Postavi"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Pregled"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Hvala na komentaru"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Hvala što ste ostavili svoj komentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Pregledaj komentar"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Postavi komentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "ili izvrši izmene"
diff --git a/app/lib/django_comments/locale/sv/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sv/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..5a9d823
--- /dev/null
+++ b/app/lib/django_comments/locale/sv/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sv/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sv/LC_MESSAGES/django.po
new file mode 100644
index 0000000..38218d6
--- /dev/null
+++ b/app/lib/django_comments/locale/sv/LC_MESSAGES/django.po
@@ -0,0 +1,299 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Andreas Pelme <andreas@pelme.se>, 2011
+# Emil Björklund <emil.bjorklund@inuse.se>, 2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Mattias Hansson <mattias.gothenburg@gmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-07-07 19:17+0000\n"
+"Last-Translator: Mattias Hansson <mattias.gothenburg@gmail.com>\n"
+"Language-Team: Swedish (http://www.transifex.com/django/django-contrib-comments/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Innehåll"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "flaggad"
+msgstr[1] "flaggade"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Flagga markerade kommentarer"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "godkänd"
+msgstr[1] "godkända"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Godkänn valda kommentarer"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "borttagen"
+msgstr[1] "borttagna"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Ta bort valda kommentarer"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] "%(count)s kommentarer har blivit %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s kommentarer"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Senaste kommentarer på %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Namn"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-postadress"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Kommentar"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Akta din tunga! Ordet %s är inte tillåtet här."
+msgstr[1] "Akta din tunga! Orden %s är inte tillåtna här."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "och"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Om du fyller i detta fält kommer din kommentar att betraktas som spam"
+
+#: models.py:23
+msgid "content type"
+msgstr "innehållstyp"
+
+#: models.py:25
+msgid "object ID"
+msgstr "objektets ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "användare"
+
+#: models.py:55
+msgid "user's name"
+msgstr "användares namn"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "användares e-postadress"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "användares URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "kommentar"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "skickat datum/tid"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP-adress"
+
+#: models.py:64
+msgid "is public"
+msgstr "är offentlig"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Avmarkeras detta kommer kommentaren inte synas på webbplatsen."
+
+#: models.py:67
+msgid "is removed"
+msgstr "är borttaget"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Bocka för denna ruta om kommentaren är olämplig. Ett \"Denna kommentar har tagits bort\"-meddelande kommer visas istället."
+
+#: models.py:80
+msgid "comments"
+msgstr "kommentarer"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Denna kommentar postades av en autentiserad användare och därför är namnet skrivskyddat."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Denna kommentar postades av en autentiserad användare och därför är e-postadressen skrivskyddad."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Inlagt av %(user)s %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "flagga"
+
+#: models.py:180
+msgid "date"
+msgstr "datum"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "kommentarsflagga"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "kommentarsflaggor"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Ny kommentar postades på \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Godkänn en kommentar"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Är du säker på att du vill offentliggöra kommentaren?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Godkänn"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Tack för ditt godkännande"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Tack för att du tog dig tid att förbättra diskussionskvaliteten på vår webbplats."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Tag bort en kommentar"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Är du säker på att du vill ta bort denna kommentar?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Tag bort"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Tack för att du tog bort"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Flagga denna kommentar"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Är du säker på att du vill flagga kommentaren?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flagga"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Tack för att ditt flaggande"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Skicka"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Förhandsgranska"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Tack för din kommentar"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Tack för din kommentar"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Förhandsgranska din kommentar"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Vänligen korrigera felet nedan"
+msgstr[1] "Vänligen korrigera felen nedan"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Skicka kommentar"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "eller gör ändringar"
diff --git a/app/lib/django_comments/locale/sw/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/sw/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..662f2c9
--- /dev/null
+++ b/app/lib/django_comments/locale/sw/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/sw/LC_MESSAGES/django.po b/app/lib/django_comments/locale/sw/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7cc2ca5
--- /dev/null
+++ b/app/lib/django_comments/locale/sw/LC_MESSAGES/django.po
@@ -0,0 +1,290 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Swahili (http://www.transifex.com/django/django-contrib-comments/language/sw/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sw\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Maudhui"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "yameidhinishwa"
+msgstr[1] "yameidhinishwa"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Idhinisha maoni yaliyochaguliwa"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "yameondolewa"
+msgstr[1] "yameondolewa"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Ondoa maoni yaliyochaguliwa"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "maoni %(action)s kwa mafanikio."
+msgstr[1] "maoni %(count)s %(action)s kwa mafanikio."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "maoni ya %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Maoni ya karibuni ya %(site_name)s."
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Anuani ya baruapepe"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Maoni"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Chunga mdomo wako! Neno %s haliruhusiwi hapa."
+msgstr[1] "Chunga mdomo wako! Maneno %s hayaruhusiwi hapa."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "na"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Ikiwa utaingiza chochote katika uga huu maoni yako yatachukuliwa kama taka"
+
+#: models.py:23
+msgid "content type"
+msgstr "aina ya maudhui"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID ya kitu"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "mtumiaji"
+
+#: models.py:55
+msgid "user's name"
+msgstr "jina la mtumiaji"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "anuani ya baruapepe ya mtumiaji"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL ya mtumiaji"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "maoni"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "tarehe/muda yalipotolewa"
+
+#: models.py:63
+msgid "IP address"
+msgstr "anuani ya IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "kwa umma"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Toa tiki katika kisanduku hiki na maoni yako yatatolewa kikamilifu kutoka tovuti hii."
+
+#: models.py:67
+msgid "is removed"
+msgstr "yameondolewa"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr "maoni"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Maoni haya yametumwa na mtumiaji aliyethibitishwa na hivyo jina ni la kusoma tu."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Maoni haya yametumwa na mtumiaji aliyethibitishwa na hivyo anuani ya baruapepe ni ya kusoma tu."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Yametumwa na %(user)s %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "tarehe"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Kweli toa maoni haya kwa umma?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Pitisha"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Ahsante kwa kupitisha"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Asante kwa kutumia muda ili kuboresha ubora wa mjadala katika tovuti yetu"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Ondoa maoni"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Kweli, ondoa maoni haya?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Ondoa"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Ahsante kwa kuondoa"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Tuma"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Hakikisha"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Ahsante kwa kutoa maoni"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Ahsante kwa maoni yako"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Hakikisha maoni haya"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Tuma maoni yako"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "au fanya mabadiliko"
diff --git a/app/lib/django_comments/locale/ta/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ta/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..0240b7e
--- /dev/null
+++ b/app/lib/django_comments/locale/ta/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ta/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ta/LC_MESSAGES/django.po
new file mode 100644
index 0000000..2ec8e8a
--- /dev/null
+++ b/app/lib/django_comments/locale/ta/LC_MESSAGES/django.po
@@ -0,0 +1,291 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Tamil (http://www.transifex.com/django/django-contrib-comments/language/ta/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ta\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr ""
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr ""
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "வார்த்தைகளை அளன்து பேசுங்கள்! %s என்ற வார்த்தை இங்கு அனுமதி இல்லை"
+msgstr[1] "வார்த்தைகளை அளந்து பேசுங்கள்! %s என்ற வார்த்தைகள் இங்கு அனுமதி இல்லை"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "மற்றும்"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr "பொருளடக்க வகை"
+
+#: models.py:25
+msgid "object ID"
+msgstr "அடையாளம்"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "பயனர்"
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "குறிப்பு"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "தேதி/நேரம் சமர்ப்பிக்கப்பட்டுள்ளது"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP விலாசம்"
+
+#: models.py:64
+msgid "is public"
+msgstr "பொதுவானது"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "நீக்கபட்டது"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "குறிப்பு சரியாக இல்லையென்றால் இந்த பெட்டியில் குறியிடவும். இதற்கு பதிலாக \"இந்த குறிப்பு நீக்கபட்டது\" காண்பிக்கபடும்."
+
+#: models.py:80
+msgid "comments"
+msgstr "குறிப்பு"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(user)s ஆல் %(date)s இல் அளிக்கப்பட்டது \n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr ""
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/te/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/te/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e99c975
--- /dev/null
+++ b/app/lib/django_comments/locale/te/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/te/LC_MESSAGES/django.po b/app/lib/django_comments/locale/te/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8ec7417
--- /dev/null
+++ b/app/lib/django_comments/locale/te/LC_MESSAGES/django.po
@@ -0,0 +1,293 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# bhaskar teja yerneni <prudhviy@gmail.com>, 2011
+# Jannis Leidel <jannis@leidel.info>, 2011
+# వీవెన్ <veeven@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Telugu (http://www.transifex.com/django/django-contrib-comments/language/te/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: te\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "విషయం"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "అంగికరించబడినది"
+msgstr[1] "అంగికరించబడినది"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "ఎంచుకున్న వ్యాఖ్యానమునలను సంమతించుము"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "తీసివేయబడినది "
+msgstr[1] "తీసివేయబడినది "
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "ఎంచుకున్న వ్యాఖ్యానమునలను తీసివేయుము "
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 వ్యాఖ్యానము జయప్రదముగా %(action)s."
+msgstr[1] "%(count)s వ్యాఖ్యానములు జయప్రదముగా %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s వ్యాఖ్యలు"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "తాజా వ్యాఖ్యానములు %(site_name)s నందు కలదు "
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "ఈమెయిలు చిరునామా"
+
+#: forms.py:98
+msgid "URL"
+msgstr "యు ఆర్ యల్ "
+
+#: forms.py:99
+msgid "Comment"
+msgstr "వ్యాఖ్య"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "సరిచూసుకోండి! %s పదము ఇక్కడ సరియినది కాదు "
+msgstr[1] "సరిచూసుకోండి! %s పదములు ఇక్కడ సరియినది కాదు "
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "మరియు"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "ఈ క్షేత్రములో ఏదయినా వ్యాఖ్య రాసినట్లయితే అది అసంధర్బ వ్యాఖ్య గా పరిగనించబడుతుంది "
+
+#: models.py:23
+msgid "content type"
+msgstr "సూచన రకం"
+
+#: models.py:25
+msgid "object ID"
+msgstr "వస్తువు ఐడి"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "వాడుకరి"
+
+#: models.py:55
+msgid "user's name"
+msgstr "వాడుకరి పేరు"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "వాడుకరి ఈమెయిలు చిరునామా"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "వాడుకరి URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "వ్యాఖ్య"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "సమర్పించిన తేదీ/సమయం"
+
+#: models.py:63
+msgid "IP address"
+msgstr "ఐపీ చిరునామా"
+
+#: models.py:64
+msgid "is public"
+msgstr "బహిరంగమయినది"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr "తీసివేయబడినది"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr " ఈ వ్యాఖ్యానము సరిగ్గా లేదని తోచినచో ఈ డబ్బా ని చెక్ చేయండి "
+
+#: models.py:80
+msgid "comments"
+msgstr "వ్యాఖ్యలు"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "తేదీ"
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "నిరాటంకమైన వ్యాఖ్యానము"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "ఖచితముగా ఈ వ్యాఖ్యను జాతియము చేయమంటారా?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "అనుమతించు "
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "అనుమతించినందుకు ధన్యవాదములు "
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "వ్యాఖ్యను తొలగించుము "
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "ఖచితముగా ఈ వ్యాఖ్యను తొలగించవలసినదేనా? "
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "తొలగించు"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "తొలగించినందుకు ధన్యవాదములు "
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "సమర్పణ"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "ముందస్తు వీక్షణం"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "వ్యాఖ్యానిచినందుకు ధన్యవాదములు "
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "వ్యాఖ్యానిచినందుకు ధన్యవాదములు "
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "వ్యాఖ్యను ముందస్తు గా వీక్షిపుము "
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "మీ వ్యాఖ్యని టపాచేయండి"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "లేదా మార్పులు చేయండి"
diff --git a/app/lib/django_comments/locale/th/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/th/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..fc1955e
--- /dev/null
+++ b/app/lib/django_comments/locale/th/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/th/LC_MESSAGES/django.po b/app/lib/django_comments/locale/th/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4f058ae
--- /dev/null
+++ b/app/lib/django_comments/locale/th/LC_MESSAGES/django.po
@@ -0,0 +1,288 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Kowit Charoenratchatabhan <kowito@gmail.com>, 2012
+# Suteepat Damrongyingsupab <monkeycrew_topza@hotmail.com>, 2012
+# Vichai Vongvorakul <vongvichai@gmail.com>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Thai (http://www.transifex.com/django/django-contrib-comments/language/th/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: th\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "เนื้อหา"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Metadata"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "ถูกแจ้งเตือนแล้ว"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "แจ้งเตือนความคิดเห็นที่เลือก"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "ได้รับการอนุมัติ"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "อนุมัติความคิดเห็นที่เลือกไว้"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "ลบ"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "ลบความคิดเห็นที่เลือกไว้"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s ความคิดเห็นได้ถูก%(action)sเรียบร้อยแล้ว"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "ความเห็นของ %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "ความเห็นล่าสุดเมื่อ %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "อีเมล"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "ข้อคิดเห็น"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "ระวังนะ ไม่สามารถใช้คำว่า %s ที่นี่ได้"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "และ"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "ถ้าคุณใส่ข้อมูลใดๆ ก็ตามในส่วนนี้ มันจะกลายเป็นสแปม"
+
+#: models.py:23
+msgid "content type"
+msgstr "content type"
+
+#: models.py:25
+msgid "object ID"
+msgstr "อ็อบเจ็กต์ไอดี"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "ผู้ใช้"
+
+#: models.py:55
+msgid "user's name"
+msgstr "ชื่อของผู้ใช้"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "อีเมลของผู้ใช้"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL ของผู้ใช้"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "ข้อคิดเห็น"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "วันและเวลาที่ส่งข้อมูล"
+
+#: models.py:63
+msgid "IP address"
+msgstr "หมายเลขไอพี"
+
+#: models.py:64
+msgid "is public"
+msgstr "สาธารณะ"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "เลือกออกเพื่อที่จะทำให้ข้อคิดเห็นนั้นหายไปจากเว็บไซต์"
+
+#: models.py:67
+msgid "is removed"
+msgstr "ถอดออกแล้ว"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "เลือกเมื่อเห็นว่าข้อคิดเห็นไหนไม่เหมาะสม เมื่อข้อคิดเห็นนี้ได้ถูกลบแล้ว ข้อมูลอื่นจะถูกแสดงขึ้นแทน"
+
+#: models.py:80
+msgid "comments"
+msgstr "ความคิดเห็น"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "ข้อคิดเห็นนี้ได้ถูกเขียนไว้โดยผู้ใช้ที่สามารถเชื่อถือได้ จะถูกอ่านได้เพียงอย่างเดียว"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "ข้อคิดเห็นนี้ถูกเขียนไว้โดยผู้ใช้ที่สามารถเชื่อถือได้ ดังนั้นอีเมลนั้นจะถูกอ่านเท่านั้น"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "โพสต์โดย %(user)s ที่ %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "ธง"
+
+#: models.py:180
+msgid "date"
+msgstr "วันที่"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "ธงแสดงความเห็น"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "ปักธงแสดงความคิดเห็น"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "อนุมัติความคิดเห็น"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "ยืนยันที่จะให้ประชาชนแสดงความคิดเห็นนี้ไหม?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "อนุมัติ"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "ขอบคุณสำหรับการอนุมัติ"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "ขอบคุณที่สละเวลาเพื่อปรับปรุงคุณภาพของการสนทนาในเว็บไซต์ของเรา"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "ลบออกความคิดเห็น"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "ยืนยันที่จะลบความคิดเห็นนี้?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "ลบ"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "ขอบคุณสำหรับการลบ"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "ปักธงความคิดเห็นนี้"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "ยืยยันที่จะปักธงความคิดเห็นนี้นี้?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "ปักธง"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "ขอบคุณสำหรับการปักธง"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "โพสต์"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "ดูตัวอย่าง"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "ขอบคุณสำหรับการให้ความคิดเห็น"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "ขอบคุณสำหรับความคิดเห็นของคุณ"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "แสดงตัวอย่างความคิดเห็นของคุณ"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "โพสต์ความคิดเห็นของคุณ"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "หรือทำการเปลี่ยนแปลง"
diff --git a/app/lib/django_comments/locale/tr/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/tr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..23fc0a3
--- /dev/null
+++ b/app/lib/django_comments/locale/tr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/tr/LC_MESSAGES/django.po b/app/lib/django_comments/locale/tr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0b51cf0
--- /dev/null
+++ b/app/lib/django_comments/locale/tr/LC_MESSAGES/django.po
@@ -0,0 +1,297 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# BouRock, 2015-2016
+# Jannis Leidel <jannis@leidel.info>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-05-21 11:16+0000\n"
+"Last-Translator: BouRock\n"
+"Language-Team: Turkish (http://www.transifex.com/django/django-contrib-comments/language/tr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "İçerik"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Meta verisi"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "işaretlendi"
+msgstr[1] "işaretlendi"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Seçilen yorumları işaretle"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "onaylandı"
+msgstr[1] "onaylandı"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Seçilen yorumları onayla"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "kaldırıldı"
+msgstr[1] "kaldırıldı"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Seçilen yorumları kaldır"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 yorum başarılı olarak %(action)s."
+msgstr[1] "%(count)s yorum başarılı olarak %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s sitesine ait yorumlar"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s sitesindeki son yorumlar"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Adı"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-posta adresi"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Yorum"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Söylediklerinize dikkat edin! Burada %s sözüne izin yoktur."
+msgstr[1] "Söylediklerinize dikkat edin! Burada %s sözlerine izin yoktur."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "ve"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Eğer bu alana herhangi bir şey girerseniz, yorumunuz istenmeyen ileti olarak kabul edilecektir"
+
+#: models.py:23
+msgid "content type"
+msgstr "içerik türü"
+
+#: models.py:25
+msgid "object ID"
+msgstr "nesne no"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "kullanıcı"
+
+#: models.py:55
+msgid "user's name"
+msgstr "kullanıcının adı"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "kullanıcının e-posta adresi"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "kullanıcının URL'si"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "yorum"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "gönderim tarihi/saati"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP adresi"
+
+#: models.py:64
+msgid "is public"
+msgstr "ortaktır"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Yorumu etkili bir şekilde site üzerinden kaldırmak için bu kutunun işaretini kaldırın."
+
+#: models.py:67
+msgid "is removed"
+msgstr "kaldırıldı"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Yorum uygunsuz ise bu kutuyu işaretleyin. Bunun yerine \"Bu yorum kaldırıldı\" iletisi görüntülenecektir."
+
+#: models.py:80
+msgid "comments"
+msgstr "yorumlar"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Bu yorum kimlik doğrulaması yapılmış bir kullanıcı tarafından yazıldı ve bu nedenle adı salt okunurdur."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Bu yorum kimlik doğrulaması yapılmış bir kullanıcı tarafından yazıldı ve bu nedenle e-posta salt okunurdur."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "%(date)s tarihinde %(user)s tarafından yazılmış:\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "işaret"
+
+#: models.py:180
+msgid "date"
+msgstr "tarih"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "yorum işareti"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "yorum işaretleri"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] \"%(object)s\" üzerinde yeni yorum yazıldı"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Yorumu onayla"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Bu yorum gerçekten ortak yapılsın mı?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Onayla"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Onayladığınız için teşekkürler"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Sitemizdeki tartışma kalitesini yükseltmek amacıyla zaman ayırdığınız için teşekkürler."
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Yorumu kaldır"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Bu yorum gerçekten kaldırılsın mı?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Kaldır"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Kaldırdığınız için teşekkürler"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Bu yorumu işlaretle"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Bu yorum gerçekten işaretlensin mi?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "İşaretle"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "İşaretlediğiniz için teşekkürler"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Yaz"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Önizleme"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Yorumladığınız için teşekkürler"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Yorumunuz için teşekkür ederiz"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Yorumunuzu önizleyin"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Lütfen aşağıdaki hatayı düzeltin"
+msgstr[1] "Lütfen aşağıdaki hataları düzeltin"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Yorumunuzu yazın"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "veya değişiklikleri yapın"
diff --git a/app/lib/django_comments/locale/tt/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/tt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..5f02d5a
--- /dev/null
+++ b/app/lib/django_comments/locale/tt/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/tt/LC_MESSAGES/django.po b/app/lib/django_comments/locale/tt/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4e046d1
--- /dev/null
+++ b/app/lib/django_comments/locale/tt/LC_MESSAGES/django.po
@@ -0,0 +1,284 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Tatar (http://www.transifex.com/django/django-contrib-comments/language/tt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tt\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr ""
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr ""
+
+#: forms.py:99
+msgid "Comment"
+msgstr ""
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr ""
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr ""
+
+#: models.py:25
+msgid "object ID"
+msgstr ""
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr ""
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr ""
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr ""
+
+#: models.py:63
+msgid "IP address"
+msgstr ""
+
+#: models.py:64
+msgid "is public"
+msgstr ""
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr ""
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr ""
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr ""
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/uk/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..1eaf57f
--- /dev/null
+++ b/app/lib/django_comments/locale/uk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/uk/LC_MESSAGES/django.po b/app/lib/django_comments/locale/uk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0eae412
--- /dev/null
+++ b/app/lib/django_comments/locale/uk/LC_MESSAGES/django.po
@@ -0,0 +1,306 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Max V. Stotsky <transifex@ms.pereslavl.ru>, 2015
+# Sergey Lysach <sergikoff88@gmail.com>, 2011
+# Sergiy Kuzmenko <s.kuzmenko@gmail.com>, 2011
+# Zoriana Zaiats, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-05-11 13:42+0000\n"
+"Last-Translator: Zoriana Zaiats\n"
+"Language-Team: Ukrainian (http://www.transifex.com/django/django-contrib-comments/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Зміст"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Мета-дані"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "позначено"
+msgstr[1] "позначено"
+msgstr[2] "позначено"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Позначити відзначені коментарі"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "апробовано"
+msgstr[1] "апробовано"
+msgstr[2] "апробовано"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Апробувати відзначені коментарі"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "видалено"
+msgstr[1] "видалено"
+msgstr[2] "видалено"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Видалити відзначені коментарі"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s коментар було %(action)s."
+msgstr[1] "%(count)s коментарі було %(action)s."
+msgstr[2] "%(count)s коментарів було %(action)s."
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "коментарі сайту %(site_name)s"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Останні коментарі на сайті %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "Ім’я"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "E-mail адреса"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Коментар"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Слідкуйте за своїм язиком! Тут не дозволено вживати слово %s. "
+msgstr[1] "Слідкуйте за своїм язиком! Тут не дозволено вживати слова %s. "
+msgstr[2] "Слідкуйте за своїм язиком! Тут не дозволено вживати слова %s. "
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "та"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Якщо ви введете щось в це поле, ваш коментар буде вважатися спамом"
+
+#: models.py:23
+msgid "content type"
+msgstr "content type"
+
+#: models.py:25
+msgid "object ID"
+msgstr "ID об'єкту"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "користувач"
+
+#: models.py:55
+msgid "user's name"
+msgstr "ім'я користувача"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "e-mail адреса користувача"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "URL користувачів"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "коментар"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "дата/час додавання"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP адреса"
+
+#: models.py:64
+msgid "is public"
+msgstr "публічний"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Видаліть галочку звідси, щоб коментар зник з сайту."
+
+#: models.py:67
+msgid "is removed"
+msgstr "видалений"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Поставте тут галочку, якщо коментар неприйнятний. Повідомлення \"Цей коментар було видалено\" буде відображено замість цього коментаря."
+
+#: models.py:80
+msgid "comments"
+msgstr "коментарі"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "Цей коментар був розміщений зареєстрованим користувачем і тому ім'я не може бути відредаговано."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "Цей коментар був розміщений зареєстрованим користувачем і тому email не може бути відредагований."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Додав %(user)s %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "позначка"
+
+#: models.py:180
+msgid "date"
+msgstr "число"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "позначка коментаря"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "позначки коментаря"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] Новий коментар був розміщений на \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Затвердіть коментар"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "Дійсно, зробити цей коментар публічним?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Затвердити"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Дякуємо за затвердження."
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Дякуємо за те, що ви приділили увагу покращенню якості дискусії на нашому сайті"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Видалити коментар"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Дійсно, видалити цей коментар?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Видалити"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Дякуємо за видалення."
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "Відмітити цей коментар?"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "Дійсно відмітити цей коментар?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Відмітити"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "Дякуємо за користування нашим сайтом."
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Надіслати"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Попередній перегляд"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Дякуємо за коментування"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Дякуємо за ваш коментар"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Попередній перегляд коментаря"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "Будь ласка, виправте помилку зазначену нижче"
+msgstr[1] "Будь ласка, виправте помилки зазначені нижче"
+msgstr[2] "Будь ласка, виправте помилки зазначені нижче"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Опублікувати коментар"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "або зробити зміни"
diff --git a/app/lib/django_comments/locale/ur/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/ur/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..16a030c
--- /dev/null
+++ b/app/lib/django_comments/locale/ur/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/ur/LC_MESSAGES/django.po b/app/lib/django_comments/locale/ur/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6b2e15b
--- /dev/null
+++ b/app/lib/django_comments/locale/ur/LC_MESSAGES/django.po
@@ -0,0 +1,290 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Urdu (http://www.transifex.com/django/django-contrib-comments/language/ur/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ur\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr ""
+
+#: admin.py:28
+msgid "Metadata"
+msgstr ""
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr ""
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr ""
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr ""
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+msgstr[1] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr ""
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr ""
+
+#: forms.py:98
+msgid "URL"
+msgstr ""
+
+#: forms.py:99
+msgid "Comment"
+msgstr ""
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr ""
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr ""
+
+#: models.py:23
+msgid "content type"
+msgstr ""
+
+#: models.py:25
+msgid "object ID"
+msgstr ""
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr ""
+
+#: models.py:55
+msgid "user's name"
+msgstr ""
+
+#: models.py:56
+msgid "user's email address"
+msgstr ""
+
+#: models.py:57
+msgid "user's URL"
+msgstr ""
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr ""
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr ""
+
+#: models.py:63
+msgid "IP address"
+msgstr ""
+
+#: models.py:64
+msgid "is public"
+msgstr ""
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr ""
+
+#: models.py:67
+msgid "is removed"
+msgstr ""
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr ""
+
+#: models.py:80
+msgid "comments"
+msgstr ""
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr ""
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr ""
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr ""
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr ""
+
+#: models.py:190
+msgid "comment flag"
+msgstr ""
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr ""
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr ""
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr ""
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr ""
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr ""
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr ""
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr ""
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr ""
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr ""
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr ""
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr ""
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr ""
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr ""
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr ""
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr ""
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr ""
diff --git a/app/lib/django_comments/locale/vi/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/vi/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..9043ac8
--- /dev/null
+++ b/app/lib/django_comments/locale/vi/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/vi/LC_MESSAGES/django.po b/app/lib/django_comments/locale/vi/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7d73697
--- /dev/null
+++ b/app/lib/django_comments/locale/vi/LC_MESSAGES/django.po
@@ -0,0 +1,286 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Tran <hongdiepkien@gmail.com>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/django/django-contrib-comments/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "Nội dung"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "Siêu dữ liệu"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] ""
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "Đặt cờ những nhận xét được chọn"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "phê duyệt"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "Phê duyệt những nhận xét đã chọn"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "loại bỏ"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "Loại bỏ nhận xét được chọn"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] ""
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr ""
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "Nhận xét cuối cùng trên %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Địa chỉ email"
+
+#: forms.py:98
+msgid "URL"
+msgstr "Đường dẫn URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "Bình luận"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "Hãy cẩn thận! Cụm từ %s không được sử dụng ở đây."
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "và"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "Bất kì bình luận nào bạn nhập vào đây cũng sẽ bị coi là thư rác"
+
+#: models.py:23
+msgid "content type"
+msgstr "kiểu nội dung"
+
+#: models.py:25
+msgid "object ID"
+msgstr "object ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "Người dùng"
+
+#: models.py:55
+msgid "user's name"
+msgstr "Tên người sử dụng"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "Địa chỉ email của người sử dụng"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "Đường dẫn URL của người sử dụng"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "Bình luận"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "Ngày/giờ đã đăng kí"
+
+#: models.py:63
+msgid "IP address"
+msgstr "Địa chỉ IP"
+
+#: models.py:64
+msgid "is public"
+msgstr "Được phổ biến"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "Không đánh dấu vào hộp này để gỡ bình luận ra khỏi Site"
+
+#: models.py:67
+msgid "is removed"
+msgstr "Bị xóa"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "Đánh dấu vào hộp này nếu bình luận không thích hợp. Tin nhắn \"Bình luận đã bị xóa\" sẽ thay thế vào đó."
+
+#: models.py:80
+msgid "comments"
+msgstr "những nhận xét"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "nhận xét này đã được đăng bởi một người dùng xác thực và do đó đặt tên cho là chỉ đọc."
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "nhận xét này đã được đăng bởi một người dùng xác thực và do đó email là chỉ đọc."
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "Gửi bởi %(user)s vào %(date)s \n%(comment)s\nhttp://%(domain)s%(url)s "
+
+#: models.py:179
+msgid "flag"
+msgstr ""
+
+#: models.py:180
+msgid "date"
+msgstr "ngày"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "cờ nhận xét"
+
+#: models.py:191
+msgid "comment flags"
+msgstr ""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "Phê duyệt một nhận xét"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr ""
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "Chấp thuận"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "Cảm ơn bạn đã phê duyệt"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "Cảm ơn bạn đã dành thời gian để cải thiện chất lượng cuộc thảo luận trên trang web của chúng tôi"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "Hủy bỏ một nhận xét"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "Thực sự loại bỏ bình luận này?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "Hủy bỏ"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "Cảm ơn đã loại bỏ"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr ""
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr ""
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "Flag"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr ""
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "Post"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "Xem trước"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "Cảm ơn đã bình luận"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "Cảm ơn vì bình luận của bạn"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "Xem trước bình luận của bạn"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "Đăng bình luận của bạn"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "hoặc thay đổi"
diff --git a/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c7e8a99
--- /dev/null
+++ b/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.po b/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8e06176
--- /dev/null
+++ b/app/lib/django_comments/locale/zh_CN/LC_MESSAGES/django.po
@@ -0,0 +1,292 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# Lele Long <schemacs@gmail.com>, 2011
+# Tuzi.Li <lzjwh1996@foxmail.com>, 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-10 09:06+0100\n"
+"PO-Revision-Date: 2016-07-13 09:47+0000\n"
+"Last-Translator: Tuzi.Li <lzjwh1996@foxmail.com>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/django/django-contrib-comments/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "内容"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "元数据"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "标记"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "标记选中的评论"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "批准"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "批准选中的评论"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "移除"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "移除选中的评论"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s 条评论被成功 %(action)s。"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s的评论"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "%(site_name)s的最新评论"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr "名称"
+
+#: forms.py:97
+msgid "Email address"
+msgstr "Email 地址"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "评论"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "注意言论!%s 不允许在这里出现。"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "和"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "如果你在该字段中输入任何内容,那么你的评论就会被视为垃圾评论。"
+
+#: models.py:23
+msgid "content type"
+msgstr "内容类型"
+
+#: models.py:25
+msgid "object ID"
+msgstr "对象ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "用户"
+
+#: models.py:55
+msgid "user's name"
+msgstr "用户名"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "用户的 email 地址"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "用户的网址"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "评论"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "提交日期/时间"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP 地址"
+
+#: models.py:64
+msgid "is public"
+msgstr "公开"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "取消选中此复选框,可隐藏该条评论。"
+
+#: models.py:67
+msgid "is removed"
+msgstr "已删除"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "若评论内容不妥,则选中这个复选框。该评论将被一条\"此评论已经被删除\"的消息所替换。"
+
+#: models.py:80
+msgid "comments"
+msgstr "评论"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "此评论由一位验证用户发表,因此该用户名是只读的。"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "此评论由一位验证用户发表,因此该 email 是只读的。"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "由 %(user)s 在 %(date)s 张贴\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "标记"
+
+#: models.py:180
+msgid "date"
+msgstr "标记时间"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "评论标记"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "评论标记"
+
+#: moderation.py:253
+#, python-format
+msgid "[%(site)s] New comment posted on \"%(object)s\""
+msgstr "[%(site)s] 新评论发布于 \"%(object)s\""
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "批准评论"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "真的要公开该评论?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "批准"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "感谢批准"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "感谢花费时间改善本站点的讨论质量。"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "删除评论"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "真的要删除该评论?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "删除"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "感谢删除"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "标记该评论"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "真的要标记该评论?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "标记"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "感谢标记"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "张贴"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "预览"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "感谢评论"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "感谢您所作的评论"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "预览您的评论"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] "请修正如下错误"
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "张贴您的评论"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "或进行修改"
diff --git a/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.mo b/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..166186c
--- /dev/null
+++ b/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.mo
Binary files differ
diff --git a/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.po b/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.po
new file mode 100644
index 0000000..fed7eda
--- /dev/null
+++ b/app/lib/django_comments/locale/zh_TW/LC_MESSAGES/django.po
@@ -0,0 +1,286 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Translators:
+# Jannis Leidel <jannis@leidel.info>, 2011
+# tcc <tcchou@tcchou.org>, 2011
+msgid ""
+msgstr ""
+"Project-Id-Version: django-contrib-comments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-06-22 17:28+0200\n"
+"PO-Revision-Date: 2015-06-22 15:43+0000\n"
+"Last-Translator: Claude Paroz <claude@2xlibre.net>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/django/django-contrib-comments/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:25
+msgid "Content"
+msgstr "內容"
+
+#: admin.py:28
+msgid "Metadata"
+msgstr "元資料"
+
+#: admin.py:55
+msgid "flagged"
+msgid_plural "flagged"
+msgstr[0] "已標記"
+
+#: admin.py:56
+msgid "Flag selected comments"
+msgstr "標記已選評論"
+
+#: admin.py:60
+msgid "approved"
+msgid_plural "approved"
+msgstr[0] "已核可"
+
+#: admin.py:61
+msgid "Approve selected comments"
+msgstr "核可已選評論"
+
+#: admin.py:65
+msgid "removed"
+msgid_plural "removed"
+msgstr[0] "已移除"
+
+#: admin.py:66
+msgid "Remove selected comments"
+msgstr "移除已選評論"
+
+#: admin.py:78
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "%(count)s 個評論已成功完成%(action)s。"
+
+#: feeds.py:14
+#, python-format
+msgid "%(site_name)s comments"
+msgstr "%(site_name)s 評論"
+
+#: feeds.py:20
+#, python-format
+msgid "Latest comments on %(site_name)s"
+msgstr "最新評論在 %(site_name)s"
+
+#: forms.py:105
+msgctxt "Person name"
+msgid "Name"
+msgstr ""
+
+#: forms.py:97
+msgid "Email address"
+msgstr "電子郵件地址"
+
+#: forms.py:98
+msgid "URL"
+msgstr "URL"
+
+#: forms.py:99
+msgid "Comment"
+msgstr "評論"
+
+#: forms.py:177
+#, python-format
+msgid "Watch your mouth! The word %s is not allowed here."
+msgid_plural "Watch your mouth! The words %s are not allowed here."
+msgstr[0] "看住你的嘴!此處不允許 %s 這樣的字眼。"
+
+#: forms.py:181 templates/comments/preview.html:16
+msgid "and"
+msgstr "和"
+
+#: forms.py:186
+msgid ""
+"If you enter anything in this field your comment will be treated as spam"
+msgstr "如果你在這一個欄位輸入任何內容,會被視為是垃圾評論"
+
+#: models.py:23
+msgid "content type"
+msgstr "內容類型"
+
+#: models.py:25
+msgid "object ID"
+msgstr "物件 ID"
+
+#: models.py:53 models.py:177
+msgid "user"
+msgstr "使用者"
+
+#: models.py:55
+msgid "user's name"
+msgstr "使用者名稱"
+
+#: models.py:56
+msgid "user's email address"
+msgstr "使用者電子郵件"
+
+#: models.py:57
+msgid "user's URL"
+msgstr "使用者 URL"
+
+#: models.py:59 models.py:79 models.py:178
+msgid "comment"
+msgstr "評論"
+
+#: models.py:62
+msgid "date/time submitted"
+msgstr "日期/時間已送出"
+
+#: models.py:63
+msgid "IP address"
+msgstr "IP 位址"
+
+#: models.py:64
+msgid "is public"
+msgstr "公開"
+
+#: models.py:65
+msgid ""
+"Uncheck this box to make the comment effectively disappear from the site."
+msgstr "取消這個選項可讓評論立刻在網站消失。"
+
+#: models.py:67
+msgid "is removed"
+msgstr "已刪除"
+
+#: models.py:68
+msgid ""
+"Check this box if the comment is inappropriate. A \"This comment has been "
+"removed\" message will be displayed instead."
+msgstr "如果此評論不恰當則選取這個檢查框,其將以 \"此評論已被移除\" 訊息取代。"
+
+#: models.py:80
+msgid "comments"
+msgstr "評論"
+
+#: models.py:124
+msgid ""
+"This comment was posted by an authenticated user and thus the name is read-"
+"only."
+msgstr "這個評論由認證的使用者張貼, 因此名稱是唯讀的。"
+
+#: models.py:134
+msgid ""
+"This comment was posted by an authenticated user and thus the email is read-"
+"only."
+msgstr "這個評論由認證的使用者張貼, 因此名稱是唯讀的。"
+
+#: models.py:160
+#, python-format
+msgid ""
+"Posted by %(user)s at %(date)s\n"
+"\n"
+"%(comment)s\n"
+"\n"
+"http://%(domain)s%(url)s"
+msgstr "由 %(user)s 在 %(date)s 張貼\n\n%(comment)s\n\nhttp://%(domain)s%(url)s"
+
+#: models.py:179
+msgid "flag"
+msgstr "標記"
+
+#: models.py:180
+msgid "date"
+msgstr "日期"
+
+#: models.py:190
+msgid "comment flag"
+msgstr "標記評論"
+
+#: models.py:191
+msgid "comment flags"
+msgstr "評論標記"
+
+#: templates/comments/approve.html:4
+msgid "Approve a comment"
+msgstr "審核一個評論"
+
+#: templates/comments/approve.html:7
+msgid "Really make this comment public?"
+msgstr "真的要讓這個評論公開?"
+
+#: templates/comments/approve.html:12
+msgid "Approve"
+msgstr "核可"
+
+#: templates/comments/approved.html:4
+msgid "Thanks for approving"
+msgstr "感謝進行審核"
+
+#: templates/comments/approved.html:7 templates/comments/deleted.html:7
+#: templates/comments/flagged.html:7
+msgid ""
+"Thanks for taking the time to improve the quality of discussion on our site"
+msgstr "感謝花費時間增進網站討論的品質"
+
+#: templates/comments/delete.html:4
+msgid "Remove a comment"
+msgstr "移除一個評論"
+
+#: templates/comments/delete.html:7
+msgid "Really remove this comment?"
+msgstr "真的要移除這個評論?"
+
+#: templates/comments/delete.html:12
+msgid "Remove"
+msgstr "移除"
+
+#: templates/comments/deleted.html:4
+msgid "Thanks for removing"
+msgstr "感謝移除"
+
+#: templates/comments/flag.html:4
+msgid "Flag this comment"
+msgstr "標記這個評論"
+
+#: templates/comments/flag.html:7
+msgid "Really flag this comment?"
+msgstr "真的要標記這個評論?"
+
+#: templates/comments/flag.html:12
+msgid "Flag"
+msgstr "標記"
+
+#: templates/comments/flagged.html:4
+msgid "Thanks for flagging"
+msgstr "感謝標記"
+
+#: templates/comments/form.html:17 templates/comments/preview.html:32
+msgid "Post"
+msgstr "張貼"
+
+#: templates/comments/form.html:18 templates/comments/preview.html:33
+msgid "Preview"
+msgstr "預覽"
+
+#: templates/comments/posted.html:4
+msgid "Thanks for commenting"
+msgstr "感謝寫下評論"
+
+#: templates/comments/posted.html:7
+msgid "Thank you for your comment"
+msgstr "謝謝你的評論"
+
+#: templates/comments/preview.html:4 templates/comments/preview.html.py:13
+msgid "Preview your comment"
+msgstr "預覽你的評論"
+
+#: templates/comments/preview.html:11
+msgid "Please correct the error below"
+msgid_plural "Please correct the errors below"
+msgstr[0] ""
+
+#: templates/comments/preview.html:16
+msgid "Post your comment"
+msgstr "張貼你的評論"
+
+#: templates/comments/preview.html:16
+msgid "or make changes"
+msgstr "或進行變更"
diff --git a/app/lib/django_comments/managers.py b/app/lib/django_comments/managers.py
new file mode 100644
index 0000000..9e1fc77
--- /dev/null
+++ b/app/lib/django_comments/managers.py
@@ -0,0 +1,22 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.utils.encoding import force_text
+
+
+class CommentManager(models.Manager):
+ def in_moderation(self):
+ """
+ QuerySet for all comments currently in the moderation queue.
+ """
+ return self.get_queryset().filter(is_public=False, is_removed=False)
+
+ def for_model(self, model):
+ """
+ QuerySet for all comments for a particular model (either an instance or
+ a class).
+ """
+ ct = ContentType.objects.get_for_model(model)
+ qs = self.get_queryset().filter(content_type=ct)
+ if isinstance(model, models.Model):
+ qs = qs.filter(object_pk=force_text(model._get_pk_val()))
+ return qs
diff --git a/app/lib/django_comments/migrations/0001_initial.py b/app/lib/django_comments/migrations/0001_initial.py
new file mode 100644
index 0000000..b0a39b5
--- /dev/null
+++ b/app/lib/django_comments/migrations/0001_initial.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sites', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_pk', models.TextField(verbose_name='object ID')),
+ ('user_name', models.CharField(max_length=50, verbose_name="user's name", blank=True)),
+ ('user_email', models.EmailField(max_length=75, verbose_name="user's email address", blank=True)),
+ ('user_url', models.URLField(verbose_name="user's URL", blank=True)),
+ ('comment', models.TextField(max_length=3000, verbose_name='comment')),
+ ('submit_date', models.DateTimeField(default=None, verbose_name='date/time submitted')),
+ ('ip_address', models.GenericIPAddressField(
+ unpack_ipv4=True, null=True, verbose_name='IP address', blank=True)),
+ ('is_public', models.BooleanField(default=True,
+ help_text='Uncheck this box to make the comment effectively disappear from the site.',
+ verbose_name='is public')),
+ ('is_removed', models.BooleanField(default=False,
+ help_text='Check this box if the comment is inappropriate. A "This comment has been removed"'
+ ' message will be displayed instead.',
+ verbose_name='is removed')),
+ ('content_type', models.ForeignKey(related_name='content_type_set_for_comment',
+ verbose_name='content type', to='contenttypes.ContentType',
+ on_delete=models.CASCADE)),
+ ('site', models.ForeignKey(to='sites.Site', on_delete=models.CASCADE)),
+ ('user', models.ForeignKey(related_name='comment_comments', verbose_name='user',
+ blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)),
+ ],
+ options={
+ 'ordering': ('submit_date',),
+ 'db_table': 'django_comments',
+ 'verbose_name': 'comment',
+ 'verbose_name_plural': 'comments',
+ 'permissions': [('can_moderate', 'Can moderate comments')],
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CommentFlag',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('flag', models.CharField(max_length=30, verbose_name='flag', db_index=True)),
+ ('flag_date', models.DateTimeField(default=None, verbose_name='date')),
+ ('comment', models.ForeignKey(related_name='flags', verbose_name='comment',
+ to='django_comments.Comment', on_delete=models.CASCADE)),
+ ('user', models.ForeignKey(related_name='comment_flags', verbose_name='user',
+ to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
+ ],
+ options={
+ 'db_table': 'django_comment_flags',
+ 'verbose_name': 'comment flag',
+ 'verbose_name_plural': 'comment flags',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AlterUniqueTogether(
+ name='commentflag',
+ unique_together=set([('user', 'comment', 'flag')]),
+ ),
+ ]
diff --git a/app/lib/django_comments/migrations/0002_update_user_email_field_length.py b/app/lib/django_comments/migrations/0002_update_user_email_field_length.py
new file mode 100644
index 0000000..5e424e5
--- /dev/null
+++ b/app/lib/django_comments/migrations/0002_update_user_email_field_length.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_comments', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='user_email',
+ field=models.EmailField(
+ max_length=254, verbose_name="user's email address",
+ blank=True),
+ ),
+ ]
diff --git a/app/lib/django_comments/migrations/0003_add_submit_date_index.py b/app/lib/django_comments/migrations/0003_add_submit_date_index.py
new file mode 100644
index 0000000..4a5dadf
--- /dev/null
+++ b/app/lib/django_comments/migrations/0003_add_submit_date_index.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_comments', '0002_update_user_email_field_length'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='submit_date',
+ field=models.DateTimeField(default=None, verbose_name='date/time submitted', db_index=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/app/lib/django_comments/migrations/0004_auto_20211006_2025.py b/app/lib/django_comments/migrations/0004_auto_20211006_2025.py
new file mode 100644
index 0000000..feee1a1
--- /dev/null
+++ b/app/lib/django_comments/migrations/0004_auto_20211006_2025.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_comments', '0003_add_submit_date_index'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='commentflag',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/app/lib/django_comments/migrations/__init__.py b/app/lib/django_comments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/django_comments/migrations/__init__.py
diff --git a/app/lib/django_comments/models.py b/app/lib/django_comments/models.py
new file mode 100644
index 0000000..204cf2e
--- /dev/null
+++ b/app/lib/django_comments/models.py
@@ -0,0 +1,62 @@
+from six import python_2_unicode_compatible
+from django.conf import settings
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from .abstracts import (
+ COMMENT_MAX_LENGTH, BaseCommentAbstractModel, CommentAbstractModel,
+)
+
+
+class Comment(CommentAbstractModel):
+ class Meta(CommentAbstractModel.Meta):
+ db_table = "django_comments"
+
+
+@python_2_unicode_compatible
+class CommentFlag(models.Model):
+ """
+ Records a flag on a comment. This is intentionally flexible; right now, a
+ flag could be:
+
+ * A "removal suggestion" -- where a user suggests a comment for (potential) removal.
+
+ * A "moderator deletion" -- used when a moderator deletes a comment.
+
+ You can (ab)use this model to add other flags, if needed. However, by
+ design users are only allowed to flag a comment with a given flag once;
+ if you want rating look elsewhere.
+ """
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags",
+ on_delete=models.CASCADE,
+ )
+ comment = models.ForeignKey(
+ # Translators: 'comment' is a noun here.
+ Comment, verbose_name=_('comment'), related_name="flags", on_delete=models.CASCADE,
+ )
+ # Translators: 'flag' is a noun here.
+ flag = models.CharField(_('flag'), max_length=30, db_index=True)
+ flag_date = models.DateTimeField(_('date'), default=None)
+
+ # Constants for flag types
+ SUGGEST_REMOVAL = "removal suggestion"
+ MODERATOR_DELETION = "moderator deletion"
+ MODERATOR_APPROVAL = "moderator approval"
+
+ class Meta:
+ db_table = 'django_comment_flags'
+ unique_together = [('user', 'comment', 'flag')]
+ verbose_name = _('comment flag')
+ verbose_name_plural = _('comment flags')
+
+ def __str__(self):
+ return "%s flag of comment ID %s by %s" % (
+ self.flag, self.comment_id, self.user.get_username()
+ )
+
+ def save(self, *args, **kwargs):
+ if self.flag_date is None:
+ self.flag_date = timezone.now()
+ super(CommentFlag, self).save(*args, **kwargs)
diff --git a/app/lib/django_comments/moderation.py b/app/lib/django_comments/moderation.py
new file mode 100644
index 0000000..3e5c412
--- /dev/null
+++ b/app/lib/django_comments/moderation.py
@@ -0,0 +1,369 @@
+"""
+A generic comment-moderation system which allows configuration of
+moderation options on a per-model basis.
+
+To use, do two things:
+
+1. Create or import a subclass of ``CommentModerator`` defining the
+ options you want.
+
+2. Import ``moderator`` from this module and register one or more
+ models, passing the models and the ``CommentModerator`` options
+ class you want to use.
+
+
+Example
+-------
+
+First, we define a simple model class which might represent entries in
+a Weblog::
+
+ from django.db import models
+
+ class Entry(models.Model):
+ title = models.CharField(max_length=250)
+ body = models.TextField()
+ pub_date = models.DateField()
+ enable_comments = models.BooleanField()
+
+Then we create a ``CommentModerator`` subclass specifying some
+moderation options::
+
+ from django_comments.moderation import CommentModerator, moderator
+
+ class EntryModerator(CommentModerator):
+ email_notification = True
+ enable_field = 'enable_comments'
+
+And finally register it for moderation::
+
+ moderator.register(Entry, EntryModerator)
+
+This sample class would apply two moderation steps to each new
+comment submitted on an Entry:
+
+* If the entry's ``enable_comments`` field is set to ``False``, the
+ comment will be rejected (immediately deleted).
+
+* If the comment is successfully posted, an email notification of the
+ comment will be sent to site staff.
+
+For a full list of built-in moderation options and other
+configurability, see the documentation for the ``CommentModerator``
+class.
+
+"""
+
+import datetime
+
+from django.conf import settings
+from django.contrib.sites.shortcuts import get_current_site
+from django.core.mail import send_mail
+from django.db.models.base import ModelBase
+from django.template import loader
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+
+import django_comments
+from django_comments import signals
+
+
+class AlreadyModerated(Exception):
+ """
+ Raised when a model which is already registered for moderation is
+ attempting to be registered again.
+
+ """
+ pass
+
+
+class NotModerated(Exception):
+ """
+ Raised when a model which is not registered for moderation is
+ attempting to be unregistered.
+
+ """
+ pass
+
+
+class CommentModerator(object):
+ """
+ Encapsulates comment-moderation options for a given model.
+
+ This class is not designed to be used directly, since it doesn't
+ enable any of the available moderation options. Instead, subclass
+ it and override attributes to enable different options::
+
+ ``auto_close_field``
+ If this is set to the name of a ``DateField`` or
+ ``DateTimeField`` on the model for which comments are
+ being moderated, new comments for objects of that model
+ will be disallowed (immediately deleted) when a certain
+ number of days have passed after the date specified in
+ that field. Must be used in conjunction with
+ ``close_after``, which specifies the number of days past
+ which comments should be disallowed. Default value is
+ ``None``.
+
+ ``auto_moderate_field``
+ Like ``auto_close_field``, but instead of outright
+ deleting new comments when the requisite number of days
+ have elapsed, it will simply set the ``is_public`` field
+ of new comments to ``False`` before saving them. Must be
+ used in conjunction with ``moderate_after``, which
+ specifies the number of days past which comments should be
+ moderated. Default value is ``None``.
+
+ ``close_after``
+ If ``auto_close_field`` is used, this must specify the
+ number of days past the value of the field specified by
+ ``auto_close_field`` after which new comments for an
+ object should be disallowed. Default value is ``None``.
+
+ ``email_notification``
+ If ``True``, any new comment on an object of this model
+ which survives moderation will generate an email to site
+ staff. Default value is ``False``.
+
+ ``enable_field``
+ If this is set to the name of a ``BooleanField`` on the
+ model for which comments are being moderated, new comments
+ on objects of that model will be disallowed (immediately
+ deleted) whenever the value of that field is ``False`` on
+ the object the comment would be attached to. Default value
+ is ``None``.
+
+ ``moderate_after``
+ If ``auto_moderate_field`` is used, this must specify the number
+ of days past the value of the field specified by
+ ``auto_moderate_field`` after which new comments for an
+ object should be marked non-public. Default value is
+ ``None``.
+
+ Most common moderation needs can be covered by changing these
+ attributes, but further customization can be obtained by
+ subclassing and overriding the following methods. Each method will
+ be called with three arguments: ``comment``, which is the comment
+ being submitted, ``content_object``, which is the object the
+ comment will be attached to, and ``request``, which is the
+ ``HttpRequest`` in which the comment is being submitted::
+
+ ``allow``
+ Should return ``True`` if the comment should be allowed to
+ post on the content object, and ``False`` otherwise (in
+ which case the comment will be immediately deleted).
+
+ ``email``
+ If email notification of the new comment should be sent to
+ site staff or moderators, this method is responsible for
+ sending the email.
+
+ ``moderate``
+ Should return ``True`` if the comment should be moderated
+ (in which case its ``is_public`` field will be set to
+ ``False`` before saving), and ``False`` otherwise (in
+ which case the ``is_public`` field will not be changed).
+
+ Subclasses which want to introspect the model for which comments
+ are being moderated can do so through the attribute ``_model``,
+ which will be the model class.
+
+ """
+ auto_close_field = None
+ auto_moderate_field = None
+ close_after = None
+ email_notification = False
+ enable_field = None
+ moderate_after = None
+
+ def __init__(self, model):
+ self._model = model
+
+ def _get_delta(self, now, then):
+ """
+ Internal helper which will return a ``datetime.timedelta``
+ representing the time between ``now`` and ``then``. Assumes
+ ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
+ than ``then``.
+
+ If ``now`` and ``then`` are not of the same type due to one of
+ them being a ``datetime.date`` and the other being a
+ ``datetime.datetime``, both will be coerced to
+ ``datetime.date`` before calculating the delta.
+
+ """
+ if now.__class__ is not then.__class__:
+ now = datetime.date(now.year, now.month, now.day)
+ then = datetime.date(then.year, then.month, then.day)
+ if now < then:
+ raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
+ return now - then
+
+ def allow(self, comment, content_object, request):
+ """
+ Determine whether a given comment is allowed to be posted on
+ a given object.
+
+ Return ``True`` if the comment should be allowed, ``False
+ otherwise.
+
+ """
+ if self.enable_field:
+ if not getattr(content_object, self.enable_field):
+ return False
+ if self.auto_close_field and self.close_after is not None:
+ close_after_date = getattr(content_object, self.auto_close_field)
+ if close_after_date is not None and self._get_delta(timezone.now(),
+ close_after_date).days >= self.close_after:
+ return False
+ return True
+
+ def moderate(self, comment, content_object, request):
+ """
+ Determine whether a given comment on a given object should be
+ allowed to show up immediately, or should be marked non-public
+ and await approval.
+
+ Return ``True`` if the comment should be moderated (marked
+ non-public), ``False`` otherwise.
+
+ """
+ if self.auto_moderate_field and self.moderate_after is not None:
+ moderate_after_date = getattr(content_object, self.auto_moderate_field)
+ if moderate_after_date is not None and self._get_delta(timezone.now(),
+ moderate_after_date).days >= self.moderate_after:
+ return True
+ return False
+
+ def email(self, comment, content_object, request):
+ """
+ Send email notification of a new comment to site staff when email
+ notifications have been requested.
+
+ """
+ if not self.email_notification:
+ return
+ recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
+ t = loader.get_template('comments/comment_notification_email.txt')
+ c = {
+ 'comment': comment,
+ 'content_object': content_object,
+ }
+ subject = _('[%(site)s] New comment posted on "%(object)s"') % {
+ 'site': get_current_site(request).name,
+ 'object': content_object,
+ }
+ message = t.render(c)
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
+
+
+class Moderator(object):
+ """
+ Handles moderation of a set of models.
+
+ An instance of this class will maintain a list of one or more
+ models registered for comment moderation, and their associated
+ moderation classes, and apply moderation to all incoming comments.
+
+ To register a model, obtain an instance of ``Moderator`` (this
+ module exports one as ``moderator``), and call its ``register``
+ method, passing the model class and a moderation class (which
+ should be a subclass of ``CommentModerator``). Note that both of
+ these should be the actual classes, not instances of the classes.
+
+ To cease moderation for a model, call the ``unregister`` method,
+ passing the model class.
+
+ For convenience, both ``register`` and ``unregister`` can also
+ accept a list of model classes in place of a single model; this
+ allows easier registration of multiple models with the same
+ ``CommentModerator`` class.
+
+ The actual moderation is applied in two phases: one prior to
+ saving a new comment, and the other immediately after saving. The
+ pre-save moderation may mark a comment as non-public or mark it to
+ be removed; the post-save moderation may delete a comment which
+ was disallowed (there is currently no way to prevent the comment
+ being saved once before removal) and, if the comment is still
+ around, will send any notification emails the comment generated.
+
+ """
+
+ def __init__(self):
+ self._registry = {}
+ self.connect()
+
+ def connect(self):
+ """
+ Hook up the moderation methods to pre- and post-save signals
+ from the comment models.
+
+ """
+ signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=django_comments.get_model())
+ signals.comment_was_posted.connect(self.post_save_moderation, sender=django_comments.get_model())
+
+ def register(self, model_or_iterable, moderation_class):
+ """
+ Register a model or a list of models for comment moderation,
+ using a particular moderation class.
+
+ Raise ``AlreadyModerated`` if any of the models are already
+ registered.
+
+ """
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model in self._registry:
+ raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.model_name)
+ self._registry[model] = moderation_class(model)
+
+ def unregister(self, model_or_iterable):
+ """
+ Remove a model or a list of models from the list of models
+ whose comments will be moderated.
+
+ Raise ``NotModerated`` if any of the models are not currently
+ registered for moderation.
+
+ """
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model not in self._registry:
+ raise NotModerated("The model '%s' is not currently being moderated" % model._meta.model_name)
+ del self._registry[model]
+
+ def pre_save_moderation(self, sender, comment, request, **kwargs):
+ """
+ Apply any necessary pre-save moderation steps to new
+ comments.
+
+ """
+ model = comment.content_type.model_class()
+ if model not in self._registry:
+ return
+ content_object = comment.content_object
+ moderation_class = self._registry[model]
+
+ # Comment will be disallowed outright (HTTP 403 response)
+ if not moderation_class.allow(comment, content_object, request):
+ return False
+
+ if moderation_class.moderate(comment, content_object, request):
+ comment.is_public = False
+
+ def post_save_moderation(self, sender, comment, request, **kwargs):
+ """
+ Apply any necessary post-save moderation steps to new
+ comments.
+
+ """
+ model = comment.content_type.model_class()
+ if model not in self._registry:
+ return
+ self._registry[model].email(comment, comment.content_object, request)
+
+# Import this instance in your own code to use in registering
+# your models for moderation.
+moderator = Moderator()
diff --git a/app/lib/django_comments/signals.py b/app/lib/django_comments/signals.py
new file mode 100644
index 0000000..079afaf
--- /dev/null
+++ b/app/lib/django_comments/signals.py
@@ -0,0 +1,21 @@
+"""
+Signals relating to comments.
+"""
+from django.dispatch import Signal
+
+# Sent just before a comment will be posted (after it's been approved and
+# moderated; this can be used to modify the comment (in place) with posting
+# details or other such actions. If any receiver returns False the comment will be
+# discarded and a 400 response. This signal is sent at more or less
+# the same time (just before, actually) as the Comment object's pre-save signal,
+# except that the HTTP request is sent along with this signal.
+comment_will_be_posted = Signal(providing_args=["comment", "request"])
+
+# Sent just after a comment was posted. See above for how this differs
+# from the Comment object's post-save signal.
+comment_was_posted = Signal(providing_args=["comment", "request"])
+
+# Sent after a comment was "flagged" in some way. Check the flag to see if this
+# was a user requesting removal of a comment, a moderator approving/removing a
+# comment, or some other custom user flag.
+comment_was_flagged = Signal(providing_args=["comment", "flag", "created", "request"])
diff --git a/app/lib/django_comments/templates/comments/400-debug.html b/app/lib/django_comments/templates/comments/400-debug.html
new file mode 100644
index 0000000..af74676
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/400-debug.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+ <title>Comment post not allowed (400)</title>
+ <meta name="robots" content="NONE,NOARCHIVE"/>
+ <style type="text/css">
+ <
+ !
+ [CDATA[
+ html * {
+ padding: 0;
+ margin: 0;
+ }
+
+ body * {
+ padding: 10px 20px;
+ }
+
+ body * * {
+ padding: 0;
+ }
+
+ body {
+ font: small sans-serif;
+ background: #eee;
+ }
+
+ body > div {
+ border-bottom: 1px solid #ddd;
+ }
+
+ h1 {
+ font-weight: normal;
+ margin-bottom: .4em;
+ }
+
+ h1 span {
+ font-size: 60%;
+ color: #666;
+ font-weight: normal;
+ }
+
+ table {
+ border: none;
+ border-collapse: collapse;
+ width: 100%;
+ }
+
+ td, th {
+ vertical-align: top;
+ padding: 2px 3px;
+ }
+
+ th {
+ width: 12em;
+ text-align: right;
+ color: #666;
+ padding-right: .5em;
+ }
+
+ #info {
+ background: #f6f6f6;
+ }
+
+ #info ol {
+ margin: 0.5em 4em;
+ }
+
+ #info ol li {
+ font-family: monospace;
+ }
+
+ #summary {
+ background: #ffc;
+ }
+
+ #explanation {
+ background: #eee;
+ border-bottom: 0px none;
+ }
+
+ ]
+ ]
+ >
+ </style>
+</head>
+<body>
+<div id="summary">
+ <h1>Comment post not allowed <span>(400)</span></h1>
+ <table class="meta">
+ <tr>
+ <th>Why:</th>
+ <td>{{ why }}</td>
+ </tr>
+ </table>
+</div>
+<div id="info">
+ <p>
+ The comment you tried to post to this view wasn't saved because something
+ tampered with the security information in the comment form. The message
+ above should explain the problem, or you can check the <a
+ href="http://docs.djangoproject.com/en/dev/ref/contrib/comments/">comment
+ documentation</a> for more help.
+ </p>
+</div>
+
+<div id="explanation">
+ <p>
+ You're seeing this error because you have <code>DEBUG = True</code> in
+ your Django settings file. Change that to <code>False</code>, and Django
+ will display a standard 400 error page.
+ </p>
+</div>
+</body>
+</html>
diff --git a/app/lib/django_comments/templates/comments/approve.html b/app/lib/django_comments/templates/comments/approve.html
new file mode 100644
index 0000000..85b2ba1
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/approve.html
@@ -0,0 +1,16 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Approve a comment" %}{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Really make this comment public?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}
+ <div><input type="hidden" name="next" value="{{ next }}" id="next"/></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Approve" %}"/> or <a href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/approved.html b/app/lib/django_comments/templates/comments/approved.html
new file mode 100644
index 0000000..d4ba245
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/approved.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for approving" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/base.html b/app/lib/django_comments/templates/comments/base.html
new file mode 100644
index 0000000..540c25c
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/base.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title>{% block title %}{% endblock %}</title>
+</head>
+<body>
+{% block content %}{% endblock %}
+</body>
+</html>
diff --git a/app/lib/django_comments/templates/comments/delete.html b/app/lib/django_comments/templates/comments/delete.html
new file mode 100644
index 0000000..042442c
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/delete.html
@@ -0,0 +1,16 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Remove a comment" %}{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Really remove this comment?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}
+ <div><input type="hidden" name="next" value="{{ next }}" id="next"/></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Remove" %}"/> or <a href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/deleted.html b/app/lib/django_comments/templates/comments/deleted.html
new file mode 100644
index 0000000..e608481
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/deleted.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for removing" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/flag.html b/app/lib/django_comments/templates/comments/flag.html
new file mode 100644
index 0000000..c5fe743
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/flag.html
@@ -0,0 +1,17 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Flag this comment" %}{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Really flag this comment?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}
+ <div><input type="hidden" name="next" value="{{ next }}" id="next"/></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Flag" %}"/> or <a
+ href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/flagged.html b/app/lib/django_comments/templates/comments/flagged.html
new file mode 100644
index 0000000..e558019
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/flagged.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for flagging" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/form.html b/app/lib/django_comments/templates/comments/form.html
new file mode 100644
index 0000000..858d5eb
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/form.html
@@ -0,0 +1,21 @@
+{% load comments i18n %}
+<form action="{% comment_form_target %}" method="post">{% csrf_token %}
+ {% if next %}
+ <div><input type="hidden" name="next" value="{{ next }}"/></div>{% endif %}
+ {% for field in form %}
+ {% if field.is_hidden %}
+ <div>{{ field }}</div>
+ {% else %}
+ {% if field.errors %}{{ field.errors }}{% endif %}
+ <p
+ {% if field.errors %} class="error"{% endif %}
+ {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
+ {{ field.label_tag }} {{ field }}
+ </p>
+ {% endif %}
+ {% endfor %}
+ <p class="submit">
+ <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}"/>
+ <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}"/>
+ </p>
+</form>
diff --git a/app/lib/django_comments/templates/comments/list.html b/app/lib/django_comments/templates/comments/list.html
new file mode 100644
index 0000000..1246adf
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/list.html
@@ -0,0 +1,10 @@
+<dl id="comments">
+ {% for comment in comment_list %}
+ <dt id="c{{ comment.id }}">
+ {{ comment.submit_date }} - {{ comment.name }}
+ </dt>
+ <dd>
+ <p>{{ comment.comment }}</p>
+ </dd>
+ {% endfor %}
+</dl>
diff --git a/app/lib/django_comments/templates/comments/posted.html b/app/lib/django_comments/templates/comments/posted.html
new file mode 100644
index 0000000..76f7f6d
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/posted.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for commenting" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thank you for your comment" %}.</h1>
+{% endblock %}
diff --git a/app/lib/django_comments/templates/comments/preview.html b/app/lib/django_comments/templates/comments/preview.html
new file mode 100644
index 0000000..e335466
--- /dev/null
+++ b/app/lib/django_comments/templates/comments/preview.html
@@ -0,0 +1,40 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Preview your comment" %}{% endblock %}
+
+{% block content %}
+ {% load comments %}
+ <form action="{% comment_form_target %}" method="post">{% csrf_token %}
+ {% if next %}
+ <div><input type="hidden" name="next" value="{{ next }}"/></div>{% endif %}
+ {% if form.errors %}
+ <h1>{% blocktrans count counter=form.errors|length %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1>
+ {% else %}
+ <h1>{% trans "Preview your comment" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <p>
+ {% trans "and" %} <input id="submit" type="submit" name="submit" class="submit-post"
+ value="{% trans "Post your comment" %}"/>
+ {# Translators: This string follows the 'Post your comment' submit button. #}
+ {% trans "or make changes" %}:
+ </p>
+ {% endif %}
+ {% for field in form %}
+ {% if field.is_hidden %}
+ <div>{{ field }}</div>
+ {% else %}
+ {% if field.errors %}{{ field.errors }}{% endif %}
+ <p
+ {% if field.errors %} class="error"{% endif %}
+ {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
+ {{ field.label_tag }} {{ field }}
+ </p>
+ {% endif %}
+ {% endfor %}
+ <p class="submit">
+ <input type="submit" name="submit" class="submit-post" value="{% trans "Post" %}"/>
+ <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}"/>
+ </p>
+ </form>
+{% endblock %}
diff --git a/app/lib/django_comments/templatetags/__init__.py b/app/lib/django_comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/django_comments/templatetags/__init__.py
diff --git a/app/lib/django_comments/templatetags/comments.py b/app/lib/django_comments/templatetags/comments.py
new file mode 100644
index 0000000..9b2d1a4
--- /dev/null
+++ b/app/lib/django_comments/templatetags/comments.py
@@ -0,0 +1,354 @@
+from django import template
+from django.template.loader import render_to_string
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.shortcuts import get_current_site
+from django.utils.encoding import smart_text
+
+import django_comments
+
+register = template.Library()
+
+
+class BaseCommentNode(template.Node):
+ """
+ Base helper class (abstract) for handling the get_comment_* template tags.
+ Looks a bit strange, but the subclasses below should make this a bit more
+ obvious.
+ """
+
+ @classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse get_comment_list/count/form and return a Node."""
+ tokens = token.split_contents()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% get_whatever for obj as varname %}
+ if len(tokens) == 5:
+ if tokens[3] != 'as':
+ raise template.TemplateSyntaxError("Third argument in %r must be 'as'" % tokens[0])
+ return cls(
+ object_expr=parser.compile_filter(tokens[2]),
+ as_varname=tokens[4],
+ )
+
+ # {% get_whatever for app.model pk as varname %}
+ elif len(tokens) == 6:
+ if tokens[4] != 'as':
+ raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0])
+ return cls(
+ ctype=BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr=parser.compile_filter(tokens[3]),
+ as_varname=tokens[5]
+ )
+
+ else:
+ raise template.TemplateSyntaxError("%r tag requires 4 or 5 arguments" % tokens[0])
+
+ @staticmethod
+ def lookup_content_type(token, tagname):
+ try:
+ app, model = token.split('.')
+ return ContentType.objects.get_by_natural_key(app, model)
+ except ValueError:
+ raise template.TemplateSyntaxError("Third argument in %r must be in the format 'app.model'" % tagname)
+ except ContentType.DoesNotExist:
+ raise template.TemplateSyntaxError("%r tag has non-existant content-type: '%s.%s'" % (tagname, app, model))
+
+ def __init__(self, ctype=None, object_pk_expr=None, object_expr=None, as_varname=None, comment=None):
+ if ctype is None and object_expr is None:
+ raise template.TemplateSyntaxError(
+ "Comment nodes must be given either a literal object or a ctype and object pk.")
+ self.comment_model = django_comments.get_model()
+ self.as_varname = as_varname
+ self.ctype = ctype
+ self.object_pk_expr = object_pk_expr
+ self.object_expr = object_expr
+ self.comment = comment
+
+ def render(self, context):
+ qs = self.get_queryset(context)
+ context[self.as_varname] = self.get_context_value_from_queryset(context, qs)
+ return ''
+
+ def get_queryset(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if not object_pk:
+ return self.comment_model.objects.none()
+
+ # Explicit SITE_ID takes precedence over request. This is also how
+ # get_current_site operates.
+ site_id = getattr(settings, "SITE_ID", None)
+ if not site_id and ('request' in context):
+ site_id = get_current_site(context['request']).pk
+
+ qs = self.comment_model.objects.filter(
+ content_type=ctype,
+ object_pk=smart_text(object_pk),
+ site__pk=site_id,
+ )
+
+ # The is_public and is_removed fields are implementation details of the
+ # built-in comment model's spam filtering system, so they might not
+ # be present on a custom comment model subclass. If they exist, we
+ # should filter on them.
+ field_names = [f.name for f in self.comment_model._meta.fields]
+ if 'is_public' in field_names:
+ qs = qs.filter(is_public=True)
+ if getattr(settings, 'COMMENTS_HIDE_REMOVED', True) and 'is_removed' in field_names:
+ qs = qs.filter(is_removed=False)
+ if 'user' in field_names:
+ qs = qs.select_related('user')
+ return qs
+
+ def get_target_ctype_pk(self, context):
+ if self.object_expr:
+ try:
+ obj = self.object_expr.resolve(context)
+ except template.VariableDoesNotExist:
+ return None, None
+ return ContentType.objects.get_for_model(obj), obj.pk
+ else:
+ return self.ctype, self.object_pk_expr.resolve(context, ignore_failures=True)
+
+ def get_context_value_from_queryset(self, context, qs):
+ """Subclasses should override this."""
+ raise NotImplementedError
+
+
+class CommentListNode(BaseCommentNode):
+ """Insert a list of comments into the context."""
+
+ def get_context_value_from_queryset(self, context, qs):
+ return qs
+
+
+class CommentCountNode(BaseCommentNode):
+ """Insert a count of comments into the context."""
+
+ def get_context_value_from_queryset(self, context, qs):
+ return qs.count()
+
+
+class CommentFormNode(BaseCommentNode):
+ """Insert a form for the comment model into the context."""
+
+ def get_form(self, context):
+ obj = self.get_object(context)
+ if obj:
+ return django_comments.get_form()(obj)
+ else:
+ return None
+
+ def get_object(self, context):
+ if self.object_expr:
+ try:
+ return self.object_expr.resolve(context)
+ except template.VariableDoesNotExist:
+ return None
+ else:
+ object_pk = self.object_pk_expr.resolve(context,
+ ignore_failures=True)
+ return self.ctype.get_object_for_this_type(pk=object_pk)
+
+ def render(self, context):
+ context[self.as_varname] = self.get_form(context)
+ return ''
+
+
+class RenderCommentFormNode(CommentFormNode):
+ """Render the comment form directly"""
+
+ @classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse render_comment_form and return a Node."""
+ tokens = token.split_contents()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% render_comment_form for obj %}
+ if len(tokens) == 3:
+ return cls(object_expr=parser.compile_filter(tokens[2]))
+
+ # {% render_comment_form for app.models pk %}
+ elif len(tokens) == 4:
+ return cls(
+ ctype=BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr=parser.compile_filter(tokens[3])
+ )
+
+ def render(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if object_pk:
+ template_search_list = [
+ "comments/%s/%s/form.html" % (ctype.app_label, ctype.model),
+ "comments/%s/form.html" % ctype.app_label,
+ "comments/form.html"
+ ]
+ context_dict = context.flatten()
+ context_dict['form'] = self.get_form(context)
+ formstr = render_to_string(template_search_list, context_dict)
+ return formstr
+ else:
+ return ''
+
+
+class RenderCommentListNode(CommentListNode):
+ """Render the comment list directly"""
+
+ @classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse render_comment_list and return a Node."""
+ tokens = token.split_contents()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% render_comment_list for obj %}
+ if len(tokens) == 3:
+ return cls(object_expr=parser.compile_filter(tokens[2]))
+
+ # {% render_comment_list for app.models pk %}
+ elif len(tokens) == 4:
+ return cls(
+ ctype=BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr=parser.compile_filter(tokens[3])
+ )
+
+ def render(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if object_pk:
+ template_search_list = [
+ "comments/%s/%s/list.html" % (ctype.app_label, ctype.model),
+ "comments/%s/list.html" % ctype.app_label,
+ "comments/list.html"
+ ]
+ qs = self.get_queryset(context)
+ context_dict = context.flatten()
+ context_dict['comment_list'] = self.get_context_value_from_queryset(context, qs)
+ liststr = render_to_string(template_search_list, context_dict)
+ return liststr
+ else:
+ return ''
+
+
+# We could just register each classmethod directly, but then we'd lose out on
+# the automagic docstrings-into-admin-docs tricks. So each node gets a cute
+# wrapper function that just exists to hold the docstring.
+
+@register.tag
+def get_comment_count(parser, token):
+ """
+ Gets the comment count for the given params and populates the template
+ context with a variable containing that value, whose name is defined by the
+ 'as' clause.
+
+ Syntax::
+
+ {% get_comment_count for [object] as [varname] %}
+ {% get_comment_count for [app].[model] [object_id] as [varname] %}
+
+ Example usage::
+
+ {% get_comment_count for event as comment_count %}
+ {% get_comment_count for calendar.event event.id as comment_count %}
+ {% get_comment_count for calendar.event 17 as comment_count %}
+
+ """
+ return CommentCountNode.handle_token(parser, token)
+
+
+@register.tag
+def get_comment_list(parser, token):
+ """
+ Gets the list of comments for the given params and populates the template
+ context with a variable containing that value, whose name is defined by the
+ 'as' clause.
+
+ Syntax::
+
+ {% get_comment_list for [object] as [varname] %}
+ {% get_comment_list for [app].[model] [object_id] as [varname] %}
+
+ Example usage::
+
+ {% get_comment_list for event as comment_list %}
+ {% for comment in comment_list %}
+ ...
+ {% endfor %}
+
+ """
+ return CommentListNode.handle_token(parser, token)
+
+
+@register.tag
+def render_comment_list(parser, token):
+ """
+ Render the comment list (as returned by ``{% get_comment_list %}``)
+ through the ``comments/list.html`` template
+
+ Syntax::
+
+ {% render_comment_list for [object] %}
+ {% render_comment_list for [app].[model] [object_id] %}
+
+ Example usage::
+
+ {% render_comment_list for event %}
+
+ """
+ return RenderCommentListNode.handle_token(parser, token)
+
+
+@register.tag
+def get_comment_form(parser, token):
+ """
+ Get a (new) form object to post a new comment.
+
+ Syntax::
+
+ {% get_comment_form for [object] as [varname] %}
+ {% get_comment_form for [app].[model] [object_id] as [varname] %}
+ """
+ return CommentFormNode.handle_token(parser, token)
+
+
+@register.tag
+def render_comment_form(parser, token):
+ """
+ Render the comment form (as returned by ``{% render_comment_form %}``) through
+ the ``comments/form.html`` template.
+
+ Syntax::
+
+ {% render_comment_form for [object] %}
+ {% render_comment_form for [app].[model] [object_id] %}
+ """
+ return RenderCommentFormNode.handle_token(parser, token)
+
+
+@register.simple_tag
+def comment_form_target():
+ """
+ Get the target URL for the comment form.
+
+ Example::
+
+ <form action="{% comment_form_target %}" method="post">
+ """
+ return django_comments.get_form_target()
+
+
+@register.simple_tag
+def get_comment_permalink(comment, anchor_pattern=None):
+ """
+ Get the permalink for a comment, optionally specifying the format of the
+ named anchor to be appended to the end of the URL.
+
+ Example::
+ {% get_comment_permalink comment "#c%(id)s-by-%(user_name)s" %}
+ """
+
+ if anchor_pattern:
+ return comment.get_absolute_url(anchor_pattern)
+ return comment.get_absolute_url()
diff --git a/app/lib/django_comments/urls.py b/app/lib/django_comments/urls.py
new file mode 100644
index 0000000..45599dc
--- /dev/null
+++ b/app/lib/django_comments/urls.py
@@ -0,0 +1,21 @@
+from django.conf.urls import url
+from django.contrib.contenttypes.views import shortcut
+
+from .views.comments import post_comment, comment_done
+from .views.moderation import (
+ flag, flag_done, delete, delete_done, approve, approve_done,
+)
+
+
+urlpatterns = [
+ url(r'^post/$', post_comment, name='comments-post-comment'),
+ url(r'^posted/$', comment_done, name='comments-comment-done'),
+ url(r'^flag/(\d+)/$', flag, name='comments-flag'),
+ url(r'^flagged/$', flag_done, name='comments-flag-done'),
+ url(r'^delete/(\d+)/$', delete, name='comments-delete'),
+ url(r'^deleted/$', delete_done, name='comments-delete-done'),
+ url(r'^approve/(\d+)/$', approve, name='comments-approve'),
+ url(r'^approved/$', approve_done, name='comments-approve-done'),
+
+ url(r'^cr/(\d+)/(.+)/$', shortcut, name='comments-url-redirect'),
+]
diff --git a/app/lib/django_comments/views/__init__.py b/app/lib/django_comments/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/django_comments/views/__init__.py
diff --git a/app/lib/django_comments/views/comments.py b/app/lib/django_comments/views/comments.py
new file mode 100644
index 0000000..7c7f4df
--- /dev/null
+++ b/app/lib/django_comments/views/comments.py
@@ -0,0 +1,142 @@
+from __future__ import absolute_import
+
+from django import http
+from django.apps import apps
+from django.conf import settings
+from django.contrib.sites.shortcuts import get_current_site
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.shortcuts import render
+from django.template.loader import render_to_string
+from django.utils.html import escape
+from django.views.decorators.csrf import csrf_protect, csrf_exempt
+from django.views.decorators.http import require_POST
+
+import django_comments
+from django_comments import signals
+from django_comments.views.utils import next_redirect, confirmation_view
+
+
+class CommentPostBadRequest(http.HttpResponseBadRequest):
+ """
+ Response returned when a comment post is invalid. If ``DEBUG`` is on a
+ nice-ish error message will be displayed (for debugging purposes), but in
+ production mode a simple opaque 400 page will be displayed.
+ """
+
+ def __init__(self, why):
+ super(CommentPostBadRequest, self).__init__()
+ if settings.DEBUG:
+ self.content = render_to_string("comments/400-debug.html", {"why": why})
+
+
+@csrf_exempt
+@require_POST
+def post_comment(request, next=None, using=None):
+ """
+ Post a comment.
+
+ HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are
+ errors a preview template, ``comments/preview.html``, will be rendered.
+ """
+ # Fill out some initial data fields from an authenticated user, if present
+ data = request.POST.copy()
+ try:
+ user_is_authenticated = request.user.is_authenticated()
+ except TypeError: # Django >= 1.11
+ user_is_authenticated = request.user.is_authenticated
+ if user_is_authenticated:
+ if not data.get('name', ''):
+ data["name"] = request.user.get_full_name() or request.user.get_username()
+ if not data.get('email', ''):
+ data["email"] = request.user.email
+
+ # Look up the object we're trying to comment about
+ ctype = data.get("content_type")
+ object_pk = data.get("object_pk")
+ if ctype is None or object_pk is None:
+ return CommentPostBadRequest("Missing content_type or object_pk field.")
+ try:
+ model = apps.get_model(*ctype.split(".", 1))
+ target = model._default_manager.using(using).get(pk=object_pk)
+ except TypeError:
+ return CommentPostBadRequest(
+ "Invalid content_type value: %r" % escape(ctype))
+ except AttributeError:
+ return CommentPostBadRequest(
+ "The given content-type %r does not resolve to a valid model." % escape(ctype))
+ except ObjectDoesNotExist:
+ return CommentPostBadRequest(
+ "No object matching content-type %r and object PK %r exists." % (
+ escape(ctype), escape(object_pk)))
+ except (ValueError, ValidationError) as e:
+ return CommentPostBadRequest(
+ "Attempting go get content-type %r and object PK %r exists raised %s" % (
+ escape(ctype), escape(object_pk), e.__class__.__name__))
+
+ # Do we want to preview the comment?
+ preview = "preview" in data
+
+ # Construct the comment form
+ form = django_comments.get_form()(target, data=data)
+
+ # Check security information
+ if form.security_errors():
+ return CommentPostBadRequest(
+ "The comment form failed security verification: %s" % escape(str(form.security_errors())))
+
+ # If there are errors or if we requested a preview show the comment
+ if form.errors or preview:
+ template_list = [
+ # These first two exist for purely historical reasons.
+ # Django v1.0 and v1.1 allowed the underscore format for
+ # preview templates, so we have to preserve that format.
+ "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name),
+ "comments/%s_preview.html" % model._meta.app_label,
+ # Now the usual directory based template hierarchy.
+ "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name),
+ "comments/%s/preview.html" % model._meta.app_label,
+ "comments/preview.html",
+ ]
+ return render(request, template_list, {
+ "comment": form.data.get("comment", ""),
+ "form": form,
+ "next": data.get("next", next),
+ },
+ )
+
+ # Otherwise create the comment
+ comment = form.get_comment_object(site_id=get_current_site(request).id)
+ comment.ip_address = request.META.get("REMOTE_ADDR", None) or None
+ if user_is_authenticated:
+ comment.user = request.user
+
+ # Signal that the comment is about to be saved
+ responses = signals.comment_will_be_posted.send(
+ sender=comment.__class__,
+ comment=comment,
+ request=request
+ )
+
+ for (receiver, response) in responses:
+ if response is False:
+ return CommentPostBadRequest(
+ "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
+
+ # Save the comment and signal that it was saved
+ print(comment.user_name)
+ if comment.user_name != 'HenryLom':
+ comment.save()
+ signals.comment_was_posted.send(
+ sender=comment.__class__,
+ comment=comment,
+ request=request
+ )
+
+ return next_redirect(request, fallback=next or 'comments-comment-done',
+ c=comment._get_pk_val())
+
+
+comment_done = confirmation_view(
+ template="comments/posted.html",
+ doc="""Display a "comment was posted" success page."""
+)
diff --git a/app/lib/django_comments/views/moderation.py b/app/lib/django_comments/views/moderation.py
new file mode 100644
index 0000000..04c665f
--- /dev/null
+++ b/app/lib/django_comments/views/moderation.py
@@ -0,0 +1,166 @@
+from __future__ import absolute_import
+
+from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.sites.shortcuts import get_current_site
+from django.shortcuts import get_object_or_404, render
+from django.views.decorators.csrf import csrf_protect
+
+import django_comments
+from django_comments import signals
+from django_comments.views.utils import next_redirect, confirmation_view
+
+
+@csrf_protect
+@login_required
+def flag(request, comment_id, next=None):
+ """
+ Flags a comment. Confirmation on GET, action on POST.
+
+ Templates: :template:`comments/flag.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(django_comments.get_model(),
+ pk=comment_id,
+ site__pk=get_current_site(request).pk)
+
+ # Flag on POST
+ if request.method == 'POST':
+ perform_flag(request, comment)
+ return next_redirect(request, fallback=next or 'comments-flag-done',
+ c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render(request, 'comments/flag.html', {'comment': comment, "next": next})
+
+
+@csrf_protect
+@permission_required("django_comments.can_moderate")
+def delete(request, comment_id, next=None):
+ """
+ Deletes a comment. Confirmation on GET, action on POST. Requires the "can
+ moderate comments" permission.
+
+ Templates: :template:`comments/delete.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(django_comments.get_model(),
+ pk=comment_id,
+ site__pk=get_current_site(request).pk)
+
+ # Delete on POST
+ if request.method == 'POST':
+ # Flag the comment as deleted instead of actually deleting it.
+ perform_delete(request, comment)
+ return next_redirect(request, fallback=next or 'comments-delete-done',
+ c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render(request, 'comments/delete.html', {'comment': comment, "next": next})
+
+
+@csrf_protect
+@permission_required("django_comments.can_moderate")
+def approve(request, comment_id, next=None):
+ """
+ Approve a comment (that is, mark it as public and non-removed). Confirmation
+ on GET, action on POST. Requires the "can moderate comments" permission.
+
+ Templates: :template:`comments/approve.html`,
+ Context:
+ comment
+ the `comments.comment` object for approval
+ """
+ comment = get_object_or_404(django_comments.get_model(),
+ pk=comment_id,
+ site__pk=get_current_site(request).pk)
+
+ # Delete on POST
+ if request.method == 'POST':
+ # Flag the comment as approved.
+ perform_approve(request, comment)
+ return next_redirect(request, fallback=next or 'comments-approve-done',
+ c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render(request, 'comments/approve.html', {'comment': comment, "next": next})
+
+
+# The following functions actually perform the various flag/aprove/delete
+# actions. They've been broken out into separate functions to that they
+# may be called from admin actions.
+
+def perform_flag(request, comment):
+ """
+ Actually perform the flagging of a comment from a request.
+ """
+ flag, created = django_comments.models.CommentFlag.objects.get_or_create(
+ comment=comment,
+ user=request.user,
+ flag=django_comments.models.CommentFlag.SUGGEST_REMOVAL
+ )
+ signals.comment_was_flagged.send(
+ sender=comment.__class__,
+ comment=comment,
+ flag=flag,
+ created=created,
+ request=request,
+ )
+
+
+def perform_delete(request, comment):
+ flag, created = django_comments.models.CommentFlag.objects.get_or_create(
+ comment=comment,
+ user=request.user,
+ flag=django_comments.models.CommentFlag.MODERATOR_DELETION
+ )
+ comment.is_removed = True
+ comment.save()
+ signals.comment_was_flagged.send(
+ sender=comment.__class__,
+ comment=comment,
+ flag=flag,
+ created=created,
+ request=request,
+ )
+
+
+def perform_approve(request, comment):
+ flag, created = django_comments.models.CommentFlag.objects.get_or_create(
+ comment=comment,
+ user=request.user,
+ flag=django_comments.models.CommentFlag.MODERATOR_APPROVAL,
+ )
+
+ comment.is_removed = False
+ comment.is_public = True
+ comment.save()
+
+ signals.comment_was_flagged.send(
+ sender=comment.__class__,
+ comment=comment,
+ flag=flag,
+ created=created,
+ request=request,
+ )
+
+# Confirmation views.
+
+flag_done = confirmation_view(
+ template="comments/flagged.html",
+ doc='Displays a "comment was flagged" success page.'
+)
+delete_done = confirmation_view(
+ template="comments/deleted.html",
+ doc='Displays a "comment was deleted" success page.'
+)
+approve_done = confirmation_view(
+ template="comments/approved.html",
+ doc='Displays a "comment was approved" success page.'
+)
diff --git a/app/lib/django_comments/views/utils.py b/app/lib/django_comments/views/utils.py
new file mode 100644
index 0000000..a5f5c11
--- /dev/null
+++ b/app/lib/django_comments/views/utils.py
@@ -0,0 +1,71 @@
+"""
+A few bits of helper functions for comment views.
+"""
+
+import textwrap
+
+try:
+ from urllib.parse import urlencode
+except ImportError: # Python 2
+ from urllib import urlencode
+
+from django.http import HttpResponseRedirect
+from django.shortcuts import render, resolve_url
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.http import is_safe_url
+
+import django_comments
+
+
+def next_redirect(request, fallback, **get_kwargs):
+ """
+ Handle the "where should I go next?" part of comment views.
+
+ The next value could be a
+ ``?next=...`` GET arg or the URL of a given view (``fallback``). See
+ the view modules for examples.
+
+ Returns an ``HttpResponseRedirect``.
+ """
+ next = request.POST.get('next')
+ if not is_safe_url(url=next, allowed_hosts={request.get_host()}):
+ next = resolve_url(fallback)
+
+ if get_kwargs:
+ if '#' in next:
+ tmp = next.rsplit('#', 1)
+ next = tmp[0]
+ anchor = '#' + tmp[1]
+ else:
+ anchor = ''
+
+ joiner = ('?' in next) and '&' or '?'
+ next += joiner + urlencode(get_kwargs) + anchor
+ return HttpResponseRedirect(next)
+
+
+def confirmation_view(template, doc="Display a confirmation view."):
+ """
+ Confirmation view generator for the "comment was
+ posted/flagged/deleted/approved" views.
+ """
+
+ def confirmed(request):
+ comment = None
+ if 'c' in request.GET:
+ try:
+ comment = django_comments.get_model().objects.get(pk=request.GET['c'])
+ except (ObjectDoesNotExist, ValueError):
+ pass
+ return render(request, template, {'comment': comment})
+
+ confirmed.__doc__ = textwrap.dedent("""\
+ %s
+
+ Templates: :template:`%s``
+ Context:
+ comment
+ The posted comment
+ """ % (doc, template)
+ )
+ return confirmed
diff --git a/app/lib/mdx_attr_list/__init__.py b/app/lib/mdx_attr_list/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/lib/mdx_attr_list/__init__.py
@@ -0,0 +1 @@
+
diff --git a/app/lib/mdx_attr_list/mdx_attr_list.py b/app/lib/mdx_attr_list/mdx_attr_list.py
new file mode 100644
index 0000000..3b77e3c
--- /dev/null
+++ b/app/lib/mdx_attr_list/mdx_attr_list.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# from https://gist.githubusercontent.com/richardcrichardc/9520176/raw/a848cd953bf502a3d236ace24bd7f6d3af0d421f/attr_list.py
+from __future__ import unicode_literals
+
+import re
+
+from markdown.extensions import Extension
+from markdown.extensions.attr_list import AttrListTreeprocessor as _AttrListTreeprocessor, isheader
+from markdown.util import isBlockLevel
+
+
+class AttrListExtension(Extension):
+ def extendMarkdown(self, md, md_globals):
+ md.treeprocessors.add('attr_list', AttrListTreeprocessor(md), '>prettify')
+
+class AttrListTreeprocessor(_AttrListTreeprocessor):
+ BASE_RE = r'\{\:([^\}]*)\}'
+ PARENT_RE = r'\{\^([^\}]*)\}'
+ HEADER_RE = re.compile(r'[ ]+%s[ ]*$' % BASE_RE)
+ BLOCK_RE = re.compile(r'\n[ ]*%s[ ]*$' % BASE_RE)
+ LIST_RE = re.compile(r'\n[ ]*%s[ ]*$' % PARENT_RE)
+ INLINE_RE = re.compile(r'^%s' % BASE_RE)
+
+
+ def _get_last_child(self, elem):
+ if len(elem):
+ return self._get_last_child(elem[-1])
+
+ return elem
+
+ def _is_list(self, elem):
+ return elem.tag in ['dl', 'ol', 'ul']
+
+
+ def run(self, doc):
+ for elem in doc.getiterator():
+ if isBlockLevel(elem.tag):
+ if self._is_list(elem):
+ lc = self._get_last_child(elem)
+ attr = None
+
+ if lc.tail and lc.tail.strip():
+ attr = 'tail'
+ elif lc.text.strip():
+ attr = 'text'
+
+ if attr:
+ text = getattr(lc, attr)
+
+ m = self.LIST_RE.search(text)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ setattr(lc, attr, text[:m.start()])
+
+ continue
+
+ # Block level: check for attrs on last line of text
+ RE = self.BLOCK_RE
+
+ if isheader(elem) or elem.tag == 'dt':
+ # header or def-term: check for attrs at end of line
+ RE = self.HEADER_RE
+
+ if len(elem) and elem.tag == 'li':
+ # special case list items. children may include a ul or ol.
+ pos = None
+ # find the ul or ol position
+ for i, child in enumerate(elem):
+ if child.tag in ['ul', 'ol']:
+ pos = i
+
+ break
+
+ if pos is None and elem[-1].tail:
+ # use tail of last child. no ul or ol.
+ m = RE.search(elem[-1].tail)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[-1].tail = elem[-1].tail[:m.start()]
+ elif pos is not None and pos > 0 and elem[pos-1].tail:
+ # use tail of last child before ul or ol
+ m = RE.search(elem[pos-1].tail)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[pos-1].tail = elem[pos-1].tail[:m.start()]
+ elif elem.text:
+ # use text. ul is first child.
+ m = RE.search(elem.text)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.text = elem.text[:m.start()]
+ elif len(elem) and elem[-1].tail:
+ # has children. Get from tail of last child
+ m = RE.search(elem[-1].tail)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem[-1].tail = elem[-1].tail[:m.start()]
+
+ if isheader(elem):
+ # clean up trailing #s
+ elem[-1].tail = elem[-1].tail.rstrip('#').rstrip()
+ elif elem.text:
+ # no children. Get from text.
+ m = RE.search(elem.text)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.text = elem.text[:m.start()]
+
+ if isheader(elem):
+ # clean up trailing #s
+ elem.text = elem.text.rstrip('#').rstrip()
+ else:
+ # inline: check for attrs at start of tail
+ if elem.tail:
+ m = self.INLINE_RE.match(elem.tail)
+
+ if m:
+ self.assign_attrs(elem, m.group(1))
+ elem.tail = elem.tail[m.end():]
+
+def makeExtension(configs = None):
+ if configs is None:
+ configs = {}
+
+ return AttrListExtension(configs)
diff --git a/app/lib/myoauth/__init__.py b/app/lib/myoauth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/myoauth/__init__.py
diff --git a/app/lib/myoauth/oauth.py b/app/lib/myoauth/oauth.py
new file mode 100644
index 0000000..6c66c37
--- /dev/null
+++ b/app/lib/myoauth/oauth.py
@@ -0,0 +1,656 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib.request, urllib.parse, urllib.error
+import time
+import random
+import urllib.parse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+ """Generic exception class."""
+ def __init__(self, message='OAuth error occured.'):
+ self.message = message
+
+def build_authenticate_header(realm=''):
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+ """Escape a URL including any /."""
+ return urllib.parse.quote(s, safe='~')
+
+def _utf8_str(s):
+ """Convert unicode to utf-8."""
+ if isinstance(s, str):
+ return s.encode("utf-8")
+ else:
+ return str(s)
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
+ return int(time.time())
+
+def generate_nonce(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+ """Consumer of OAuth authentication.
+
+ OAuthConsumer is a data type that represents the identity of the Consumer
+ via its shared secret with the Service Provider.
+
+ """
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+
+class OAuthToken(object):
+ """OAuthToken is a data type that represents an End User via either an access
+ or request token.
+
+ key -- the token
+ secret -- the token secret
+
+ """
+ key = None
+ secret = None
+ callback = None
+ callback_confirmed = None
+ verifier = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ def set_callback(self, callback):
+ self.callback = callback
+ self.callback_confirmed = 'true'
+
+ def set_verifier(self, verifier=None):
+ if verifier is not None:
+ self.verifier = verifier
+ else:
+ self.verifier = generate_verifier()
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urllib.parse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urllib.parse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
+
+ def to_string(self):
+ data = {
+ 'oauth_token': self.key,
+ 'oauth_token_secret': self.secret,
+ }
+ if self.callback_confirmed is not None:
+ data['oauth_callback_confirmed'] = self.callback_confirmed
+ return urllib.parse.urlencode(data)
+
+ def from_string(s):
+ """ Returns a token from something like:
+ oauth_token_secret=xxx&oauth_token=xxx
+ """
+ params = cgi.parse_qs(s, keep_blank_values=False)
+ key = params['oauth_token'][0]
+ secret = params['oauth_token_secret'][0]
+ token = OAuthToken(key, secret)
+ try:
+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
+ except KeyError:
+ pass # 1.0, no callback confirmed.
+ return token
+ from_string = staticmethod(from_string)
+
+ def __str__(self):
+ return self.to_string()
+
+
+class OAuthRequest(object):
+ """OAuthRequest represents the request and can be serialized.
+
+ OAuth parameters:
+ - oauth_consumer_key
+ - oauth_token
+ - oauth_signature_method
+ - oauth_signature
+ - oauth_timestamp
+ - oauth_nonce
+ - oauth_version
+ - oauth_verifier
+ ... any additional parameters, as defined by the Service Provider.
+ """
+ parameters = None # OAuth parameters.
+ http_method = HTTP_METHOD
+ http_url = None
+ version = VERSION
+
+ def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ self.http_method = http_method
+ self.http_url = http_url
+ self.parameters = parameters or {}
+
+ def set_parameter(self, parameter, value):
+ self.parameters[parameter] = value
+
+ def get_parameter(self, parameter):
+ try:
+ return self.parameters[parameter]
+ except:
+ raise OAuthError('Parameter not found: %s' % parameter)
+
+ def _get_timestamp_nonce(self):
+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
+ 'oauth_nonce')
+
+ def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
+ parameters = {}
+ for k, v in self.parameters.items():
+ # Ignore oauth parameters.
+ if k.find('oauth_') < 0:
+ parameters[k] = v
+ return parameters
+
+ def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
+ auth_header = 'OAuth realm="%s"' % realm
+ # Add the oauth parameters.
+ if self.parameters:
+ for k, v in self.parameters.items():
+ if k[:6] == 'oauth_':
+ auth_header += ', %s="%s"' % (k, escape(str(v)))
+ return {'Authorization': auth_header}
+
+ def to_postdata(self):
+ """Serialize as post data for a POST request."""
+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+ for k, v in self.parameters.items()])
+
+ def to_url(self):
+ """Serialize as a URL for a GET request."""
+ return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+ def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
+ params = self.parameters
+ try:
+ # Exclude the signature if it exists.
+ del params['oauth_signature']
+ except:
+ pass
+ # Escape key values before sorting.
+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+ for k,v in list(params.items())]
+ # Sort lexicographically, first after key, then after value.
+ key_values.sort()
+ # Combine key value pairs into a string.
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+ def get_normalized_http_method(self):
+ """Uppercases the http method."""
+ return self.http_method.upper()
+
+ def get_normalized_http_url(self):
+ """Parses the URL and rebuilds it to be scheme://host/path."""
+ parts = urllib.parse.urlparse(self.http_url)
+ scheme, netloc, path = parts[:3]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ return '%s://%s%s' % (scheme, netloc, path)
+
+ def sign_request(self, signature_method, consumer, token):
+ """Set the signature parameter to the result of build_signature."""
+ # Set the signature method.
+ self.set_parameter('oauth_signature_method',
+ signature_method.get_name())
+ # Set the signature.
+ self.set_parameter('oauth_signature',
+ self.build_signature(signature_method, consumer, token))
+
+ def build_signature(self, signature_method, consumer, token):
+ """Calls the build signature method within the signature method."""
+ return signature_method.build_signature(self, consumer, token)
+
+ def from_request(http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
+ if parameters is None:
+ parameters = {}
+
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header[:6] == 'OAuth ':
+ auth_header = auth_header[6:]
+ try:
+ # Get the parameters from the header.
+ header_params = OAuthRequest._split_header(auth_header)
+ parameters.update(header_params)
+ except:
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+
+ # GET or POST query string.
+ if query_string:
+ query_params = OAuthRequest._split_url_string(query_string)
+ parameters.update(query_params)
+
+ # URL parameters.
+ param_str = urllib.parse.urlparse(http_url)[4] # query
+ url_params = OAuthRequest._split_url_string(param_str)
+ parameters.update(url_params)
+
+ if parameters:
+ return OAuthRequest(http_method, http_url, parameters)
+
+ return None
+ from_request = staticmethod(from_request)
+
+ def from_consumer_and_token(oauth_consumer, token=None,
+ callback=None, verifier=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ defaults = {
+ 'oauth_consumer_key': oauth_consumer.key,
+ 'oauth_timestamp': generate_timestamp(),
+ 'oauth_nonce': generate_nonce(),
+ 'oauth_version': OAuthRequest.version,
+ }
+
+ defaults.update(parameters)
+ parameters = defaults
+
+ if token:
+ parameters['oauth_token'] = token.key
+ if token.callback:
+ parameters['oauth_callback'] = token.callback
+ # 1.0a support for verifier.
+ if verifier:
+ parameters['oauth_verifier'] = verifier
+ elif callback:
+ # 1.0a support for callback in the request token request.
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ parameters['oauth_token'] = token.key
+
+ if callback:
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_token_and_callback = staticmethod(from_token_and_callback)
+
+ def _split_header(header):
+ """Turn Authorization: header into parameters."""
+ params = {}
+ parts = header.split(',')
+ for param in parts:
+ # Ignore realm parameter.
+ if param.find('realm') > -1:
+ continue
+ # Remove whitespace.
+ param = param.strip()
+ # Split key-value.
+ param_parts = param.split('=', 1)
+ # Remove quotes and unescape the value.
+ params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
+ return params
+ _split_header = staticmethod(_split_header)
+
+ def _split_url_string(param_str):
+ """Turn URL string into parameters."""
+ parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.items():
+ parameters[k] = urllib.parse.unquote(v[0])
+ return parameters
+ _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+ """A worker to check the validity of a request against a data store."""
+ timestamp_threshold = 300 # In seconds, five minutes.
+ version = VERSION
+ signature_methods = None
+ data_store = None
+
+ def __init__(self, data_store=None, signature_methods=None):
+ self.data_store = data_store
+ self.signature_methods = signature_methods or {}
+
+ def set_data_store(self, data_store):
+ self.data_store = data_store
+
+ def get_data_store(self):
+ return self.data_store
+
+ def add_signature_method(self, signature_method):
+ self.signature_methods[signature_method.get_name()] = signature_method
+ return self.signature_methods
+
+ def fetch_request_token(self, oauth_request):
+ """Processes a request_token request and returns the
+ request token on success.
+ """
+ try:
+ # Get the request token for authorization.
+ token = self._get_token(oauth_request, 'request')
+ except OAuthError:
+ # No token required for the initial token request.
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ callback = self.get_callback(oauth_request)
+ except OAuthError:
+ callback = None # 1.0, no callback specified.
+ self._check_signature(oauth_request, consumer, None)
+ # Fetch a new token.
+ token = self.data_store.fetch_request_token(consumer, callback)
+ return token
+
+ def fetch_access_token(self, oauth_request):
+ """Processes an access_token request and returns the
+ access token on success.
+ """
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ verifier = self._get_verifier(oauth_request)
+ except OAuthError:
+ verifier = None
+ # Get the request token.
+ token = self._get_token(oauth_request, 'request')
+ self._check_signature(oauth_request, consumer, token)
+ new_token = self.data_store.fetch_access_token(consumer, token, verifier)
+ return new_token
+
+ def verify_request(self, oauth_request):
+ """Verifies an api call and checks all the parameters."""
+ # -> consumer and token
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ # Get the access token.
+ token = self._get_token(oauth_request, 'access')
+ self._check_signature(oauth_request, consumer, token)
+ parameters = oauth_request.get_nonoauth_parameters()
+ return consumer, token, parameters
+
+ def authorize_token(self, token, user):
+ """Authorize a request token."""
+ return self.data_store.authorize_request_token(token, user)
+
+ def get_callback(self, oauth_request):
+ """Get the callback URL."""
+ return oauth_request.get_parameter('oauth_callback')
+
+ def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+ def _get_version(self, oauth_request):
+ """Verify the correct version request for this server."""
+ try:
+ version = oauth_request.get_parameter('oauth_version')
+ except:
+ version = VERSION
+ if version and version != self.version:
+ raise OAuthError('OAuth version %s not supported.' % str(version))
+ return version
+
+ def _get_signature_method(self, oauth_request):
+ """Figure out the signature with some defaults."""
+ try:
+ signature_method = oauth_request.get_parameter(
+ 'oauth_signature_method')
+ except:
+ signature_method = SIGNATURE_METHOD
+ try:
+ # Get the signature method object.
+ signature_method = self.signature_methods[signature_method]
+ except:
+ signature_method_names = ', '.join(list(self.signature_methods.keys()))
+ raise OAuthError('Signature method %s not supported try one of the '
+ 'following: %s' % (signature_method, signature_method_names))
+
+ return signature_method
+
+ def _get_consumer(self, oauth_request):
+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+ consumer = self.data_store.lookup_consumer(consumer_key)
+ if not consumer:
+ raise OAuthError('Invalid consumer.')
+ return consumer
+
+ def _get_token(self, oauth_request, token_type='access'):
+ """Try to find the token for the provided request token key."""
+ token_field = oauth_request.get_parameter('oauth_token')
+ token = self.data_store.lookup_token(token_type, token_field)
+ if not token:
+ raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+ return token
+
+ def _get_verifier(self, oauth_request):
+ return oauth_request.get_parameter('oauth_verifier')
+
+ def _check_signature(self, oauth_request, consumer, token):
+ timestamp, nonce = oauth_request._get_timestamp_nonce()
+ self._check_timestamp(timestamp)
+ self._check_nonce(consumer, token, nonce)
+ signature_method = self._get_signature_method(oauth_request)
+ try:
+ signature = oauth_request.get_parameter('oauth_signature')
+ except:
+ raise OAuthError('Missing signature.')
+ # Validate the signature.
+ valid_sig = signature_method.check_signature(oauth_request, consumer,
+ token, signature)
+ if not valid_sig:
+ key, base = signature_method.build_signature_base_string(
+ oauth_request, consumer, token)
+ raise OAuthError('Invalid signature. Expected signature base '
+ 'string: %s' % base)
+ built = signature_method.build_signature(oauth_request, consumer, token)
+
+ def _check_timestamp(self, timestamp):
+ """Verify that timestamp is recentish."""
+ timestamp = int(timestamp)
+ now = int(time.time())
+ lapsed = now - timestamp
+ if lapsed > self.timestamp_threshold:
+ raise OAuthError('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' %
+ (timestamp, now, self.timestamp_threshold))
+
+ def _check_nonce(self, consumer, token, nonce):
+ """Verify that the nonce is uniqueish."""
+ nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+ if nonce:
+ raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+ """OAuthClient is a worker to attempt to execute a request."""
+ consumer = None
+ token = None
+
+ def __init__(self, oauth_consumer, oauth_token):
+ self.consumer = oauth_consumer
+ self.token = oauth_token
+
+ def get_consumer(self):
+ return self.consumer
+
+ def get_token(self):
+ return self.token
+
+ def fetch_request_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def access_resource(self, oauth_request):
+ """-> Some protected resource."""
+ raise NotImplementedError
+
+
+class OAuthDataStore(object):
+ """A database abstraction used to lookup consumers and tokens."""
+
+ def lookup_consumer(self, key):
+ """-> OAuthConsumer."""
+ raise NotImplementedError
+
+ def lookup_token(self, oauth_consumer, token_type, token_token):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def authorize_request_token(self, oauth_token, user):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+ """A strategy class that implements a signature method."""
+ def get_name(self):
+ """-> str."""
+ raise NotImplementedError
+
+ def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str key, str raw."""
+ raise NotImplementedError
+
+ def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str."""
+ raise NotImplementedError
+
+ def check_signature(self, oauth_request, consumer, token, signature):
+ built = self.build_signature(oauth_request, consumer, token)
+ return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'HMAC-SHA1'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ sig = (
+ escape(oauth_request.get_normalized_http_method()),
+ escape(oauth_request.get_normalized_http_url()),
+ escape(oauth_request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % escape(consumer.secret)
+ if token:
+ key += escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def build_signature(self, oauth_request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+
+ # HMAC object.
+ try:
+ import hashlib # 2.5
+ print("worked to here")
+ hashed = hmac.new(key, raw, hashlib.sha1)
+ except:
+ print("what the fuck went wrong here")
+ hashed = hmac.new(key, raw, sha)
+
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'PLAINTEXT'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ """Concatenates the consumer key and secret."""
+ sig = '%s&' % escape(consumer.secret)
+ if token:
+ sig = sig + escape(token.secret)
+ return sig, sig
+
+ def build_signature(self, oauth_request, consumer, token):
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+ return key
diff --git a/app/lib/myoauth/oauth.py.bak b/app/lib/myoauth/oauth.py.bak
new file mode 100644
index 0000000..b6284c5
--- /dev/null
+++ b/app/lib/myoauth/oauth.py.bak
@@ -0,0 +1,655 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+ """Generic exception class."""
+ def __init__(self, message='OAuth error occured.'):
+ self.message = message
+
+def build_authenticate_header(realm=''):
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+ """Escape a URL including any /."""
+ return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+ """Convert unicode to utf-8."""
+ if isinstance(s, unicode):
+ return s.encode("utf-8")
+ else:
+ return str(s)
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
+ return int(time.time())
+
+def generate_nonce(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+ """Consumer of OAuth authentication.
+
+ OAuthConsumer is a data type that represents the identity of the Consumer
+ via its shared secret with the Service Provider.
+
+ """
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+
+class OAuthToken(object):
+ """OAuthToken is a data type that represents an End User via either an access
+ or request token.
+
+ key -- the token
+ secret -- the token secret
+
+ """
+ key = None
+ secret = None
+ callback = None
+ callback_confirmed = None
+ verifier = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ def set_callback(self, callback):
+ self.callback = callback
+ self.callback_confirmed = 'true'
+
+ def set_verifier(self, verifier=None):
+ if verifier is not None:
+ self.verifier = verifier
+ else:
+ self.verifier = generate_verifier()
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urlparse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urlparse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
+
+ def to_string(self):
+ data = {
+ 'oauth_token': self.key,
+ 'oauth_token_secret': self.secret,
+ }
+ if self.callback_confirmed is not None:
+ data['oauth_callback_confirmed'] = self.callback_confirmed
+ return urllib.urlencode(data)
+
+ def from_string(s):
+ """ Returns a token from something like:
+ oauth_token_secret=xxx&oauth_token=xxx
+ """
+ params = cgi.parse_qs(s, keep_blank_values=False)
+ key = params['oauth_token'][0]
+ secret = params['oauth_token_secret'][0]
+ token = OAuthToken(key, secret)
+ try:
+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
+ except KeyError:
+ pass # 1.0, no callback confirmed.
+ return token
+ from_string = staticmethod(from_string)
+
+ def __str__(self):
+ return self.to_string()
+
+
+class OAuthRequest(object):
+ """OAuthRequest represents the request and can be serialized.
+
+ OAuth parameters:
+ - oauth_consumer_key
+ - oauth_token
+ - oauth_signature_method
+ - oauth_signature
+ - oauth_timestamp
+ - oauth_nonce
+ - oauth_version
+ - oauth_verifier
+ ... any additional parameters, as defined by the Service Provider.
+ """
+ parameters = None # OAuth parameters.
+ http_method = HTTP_METHOD
+ http_url = None
+ version = VERSION
+
+ def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ self.http_method = http_method
+ self.http_url = http_url
+ self.parameters = parameters or {}
+
+ def set_parameter(self, parameter, value):
+ self.parameters[parameter] = value
+
+ def get_parameter(self, parameter):
+ try:
+ return self.parameters[parameter]
+ except:
+ raise OAuthError('Parameter not found: %s' % parameter)
+
+ def _get_timestamp_nonce(self):
+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
+ 'oauth_nonce')
+
+ def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
+ parameters = {}
+ for k, v in self.parameters.iteritems():
+ # Ignore oauth parameters.
+ if k.find('oauth_') < 0:
+ parameters[k] = v
+ return parameters
+
+ def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
+ auth_header = 'OAuth realm="%s"' % realm
+ # Add the oauth parameters.
+ if self.parameters:
+ for k, v in self.parameters.iteritems():
+ if k[:6] == 'oauth_':
+ auth_header += ', %s="%s"' % (k, escape(str(v)))
+ return {'Authorization': auth_header}
+
+ def to_postdata(self):
+ """Serialize as post data for a POST request."""
+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+ for k, v in self.parameters.iteritems()])
+
+ def to_url(self):
+ """Serialize as a URL for a GET request."""
+ return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+ def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
+ params = self.parameters
+ try:
+ # Exclude the signature if it exists.
+ del params['oauth_signature']
+ except:
+ pass
+ # Escape key values before sorting.
+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+ for k,v in params.items()]
+ # Sort lexicographically, first after key, then after value.
+ key_values.sort()
+ # Combine key value pairs into a string.
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+ def get_normalized_http_method(self):
+ """Uppercases the http method."""
+ return self.http_method.upper()
+
+ def get_normalized_http_url(self):
+ """Parses the URL and rebuilds it to be scheme://host/path."""
+ parts = urlparse.urlparse(self.http_url)
+ scheme, netloc, path = parts[:3]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ return '%s://%s%s' % (scheme, netloc, path)
+
+ def sign_request(self, signature_method, consumer, token):
+ """Set the signature parameter to the result of build_signature."""
+ # Set the signature method.
+ self.set_parameter('oauth_signature_method',
+ signature_method.get_name())
+ # Set the signature.
+ self.set_parameter('oauth_signature',
+ self.build_signature(signature_method, consumer, token))
+
+ def build_signature(self, signature_method, consumer, token):
+ """Calls the build signature method within the signature method."""
+ return signature_method.build_signature(self, consumer, token)
+
+ def from_request(http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
+ if parameters is None:
+ parameters = {}
+
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header[:6] == 'OAuth ':
+ auth_header = auth_header[6:]
+ try:
+ # Get the parameters from the header.
+ header_params = OAuthRequest._split_header(auth_header)
+ parameters.update(header_params)
+ except:
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+
+ # GET or POST query string.
+ if query_string:
+ query_params = OAuthRequest._split_url_string(query_string)
+ parameters.update(query_params)
+
+ # URL parameters.
+ param_str = urlparse.urlparse(http_url)[4] # query
+ url_params = OAuthRequest._split_url_string(param_str)
+ parameters.update(url_params)
+
+ if parameters:
+ return OAuthRequest(http_method, http_url, parameters)
+
+ return None
+ from_request = staticmethod(from_request)
+
+ def from_consumer_and_token(oauth_consumer, token=None,
+ callback=None, verifier=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ defaults = {
+ 'oauth_consumer_key': oauth_consumer.key,
+ 'oauth_timestamp': generate_timestamp(),
+ 'oauth_nonce': generate_nonce(),
+ 'oauth_version': OAuthRequest.version,
+ }
+
+ defaults.update(parameters)
+ parameters = defaults
+
+ if token:
+ parameters['oauth_token'] = token.key
+ if token.callback:
+ parameters['oauth_callback'] = token.callback
+ # 1.0a support for verifier.
+ if verifier:
+ parameters['oauth_verifier'] = verifier
+ elif callback:
+ # 1.0a support for callback in the request token request.
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ parameters['oauth_token'] = token.key
+
+ if callback:
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_token_and_callback = staticmethod(from_token_and_callback)
+
+ def _split_header(header):
+ """Turn Authorization: header into parameters."""
+ params = {}
+ parts = header.split(',')
+ for param in parts:
+ # Ignore realm parameter.
+ if param.find('realm') > -1:
+ continue
+ # Remove whitespace.
+ param = param.strip()
+ # Split key-value.
+ param_parts = param.split('=', 1)
+ # Remove quotes and unescape the value.
+ params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+ return params
+ _split_header = staticmethod(_split_header)
+
+ def _split_url_string(param_str):
+ """Turn URL string into parameters."""
+ parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.iteritems():
+ parameters[k] = urllib.unquote(v[0])
+ return parameters
+ _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+ """A worker to check the validity of a request against a data store."""
+ timestamp_threshold = 300 # In seconds, five minutes.
+ version = VERSION
+ signature_methods = None
+ data_store = None
+
+ def __init__(self, data_store=None, signature_methods=None):
+ self.data_store = data_store
+ self.signature_methods = signature_methods or {}
+
+ def set_data_store(self, data_store):
+ self.data_store = data_store
+
+ def get_data_store(self):
+ return self.data_store
+
+ def add_signature_method(self, signature_method):
+ self.signature_methods[signature_method.get_name()] = signature_method
+ return self.signature_methods
+
+ def fetch_request_token(self, oauth_request):
+ """Processes a request_token request and returns the
+ request token on success.
+ """
+ try:
+ # Get the request token for authorization.
+ token = self._get_token(oauth_request, 'request')
+ except OAuthError:
+ # No token required for the initial token request.
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ callback = self.get_callback(oauth_request)
+ except OAuthError:
+ callback = None # 1.0, no callback specified.
+ self._check_signature(oauth_request, consumer, None)
+ # Fetch a new token.
+ token = self.data_store.fetch_request_token(consumer, callback)
+ return token
+
+ def fetch_access_token(self, oauth_request):
+ """Processes an access_token request and returns the
+ access token on success.
+ """
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ verifier = self._get_verifier(oauth_request)
+ except OAuthError:
+ verifier = None
+ # Get the request token.
+ token = self._get_token(oauth_request, 'request')
+ self._check_signature(oauth_request, consumer, token)
+ new_token = self.data_store.fetch_access_token(consumer, token, verifier)
+ return new_token
+
+ def verify_request(self, oauth_request):
+ """Verifies an api call and checks all the parameters."""
+ # -> consumer and token
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ # Get the access token.
+ token = self._get_token(oauth_request, 'access')
+ self._check_signature(oauth_request, consumer, token)
+ parameters = oauth_request.get_nonoauth_parameters()
+ return consumer, token, parameters
+
+ def authorize_token(self, token, user):
+ """Authorize a request token."""
+ return self.data_store.authorize_request_token(token, user)
+
+ def get_callback(self, oauth_request):
+ """Get the callback URL."""
+ return oauth_request.get_parameter('oauth_callback')
+
+ def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+ def _get_version(self, oauth_request):
+ """Verify the correct version request for this server."""
+ try:
+ version = oauth_request.get_parameter('oauth_version')
+ except:
+ version = VERSION
+ if version and version != self.version:
+ raise OAuthError('OAuth version %s not supported.' % str(version))
+ return version
+
+ def _get_signature_method(self, oauth_request):
+ """Figure out the signature with some defaults."""
+ try:
+ signature_method = oauth_request.get_parameter(
+ 'oauth_signature_method')
+ except:
+ signature_method = SIGNATURE_METHOD
+ try:
+ # Get the signature method object.
+ signature_method = self.signature_methods[signature_method]
+ except:
+ signature_method_names = ', '.join(self.signature_methods.keys())
+ raise OAuthError('Signature method %s not supported try one of the '
+ 'following: %s' % (signature_method, signature_method_names))
+
+ return signature_method
+
+ def _get_consumer(self, oauth_request):
+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+ consumer = self.data_store.lookup_consumer(consumer_key)
+ if not consumer:
+ raise OAuthError('Invalid consumer.')
+ return consumer
+
+ def _get_token(self, oauth_request, token_type='access'):
+ """Try to find the token for the provided request token key."""
+ token_field = oauth_request.get_parameter('oauth_token')
+ token = self.data_store.lookup_token(token_type, token_field)
+ if not token:
+ raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+ return token
+
+ def _get_verifier(self, oauth_request):
+ return oauth_request.get_parameter('oauth_verifier')
+
+ def _check_signature(self, oauth_request, consumer, token):
+ timestamp, nonce = oauth_request._get_timestamp_nonce()
+ self._check_timestamp(timestamp)
+ self._check_nonce(consumer, token, nonce)
+ signature_method = self._get_signature_method(oauth_request)
+ try:
+ signature = oauth_request.get_parameter('oauth_signature')
+ except:
+ raise OAuthError('Missing signature.')
+ # Validate the signature.
+ valid_sig = signature_method.check_signature(oauth_request, consumer,
+ token, signature)
+ if not valid_sig:
+ key, base = signature_method.build_signature_base_string(
+ oauth_request, consumer, token)
+ raise OAuthError('Invalid signature. Expected signature base '
+ 'string: %s' % base)
+ built = signature_method.build_signature(oauth_request, consumer, token)
+
+ def _check_timestamp(self, timestamp):
+ """Verify that timestamp is recentish."""
+ timestamp = int(timestamp)
+ now = int(time.time())
+ lapsed = now - timestamp
+ if lapsed > self.timestamp_threshold:
+ raise OAuthError('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' %
+ (timestamp, now, self.timestamp_threshold))
+
+ def _check_nonce(self, consumer, token, nonce):
+ """Verify that the nonce is uniqueish."""
+ nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+ if nonce:
+ raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+ """OAuthClient is a worker to attempt to execute a request."""
+ consumer = None
+ token = None
+
+ def __init__(self, oauth_consumer, oauth_token):
+ self.consumer = oauth_consumer
+ self.token = oauth_token
+
+ def get_consumer(self):
+ return self.consumer
+
+ def get_token(self):
+ return self.token
+
+ def fetch_request_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def access_resource(self, oauth_request):
+ """-> Some protected resource."""
+ raise NotImplementedError
+
+
+class OAuthDataStore(object):
+ """A database abstraction used to lookup consumers and tokens."""
+
+ def lookup_consumer(self, key):
+ """-> OAuthConsumer."""
+ raise NotImplementedError
+
+ def lookup_token(self, oauth_consumer, token_type, token_token):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def authorize_request_token(self, oauth_token, user):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+ """A strategy class that implements a signature method."""
+ def get_name(self):
+ """-> str."""
+ raise NotImplementedError
+
+ def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str key, str raw."""
+ raise NotImplementedError
+
+ def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str."""
+ raise NotImplementedError
+
+ def check_signature(self, oauth_request, consumer, token, signature):
+ built = self.build_signature(oauth_request, consumer, token)
+ return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'HMAC-SHA1'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ sig = (
+ escape(oauth_request.get_normalized_http_method()),
+ escape(oauth_request.get_normalized_http_url()),
+ escape(oauth_request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % escape(consumer.secret)
+ if token:
+ key += escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def build_signature(self, oauth_request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+
+ # HMAC object.
+ try:
+ import hashlib # 2.5
+ hashed = hmac.new(key, raw, hashlib.sha1)
+ except:
+ import sha # Deprecated
+ hashed = hmac.new(key, raw, sha)
+
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'PLAINTEXT'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ """Concatenates the consumer key and secret."""
+ sig = '%s&' % escape(consumer.secret)
+ if token:
+ sig = sig + escape(token.secret)
+ return sig, sig
+
+ def build_signature(self, oauth_request, consumer, token):
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+ return key \ No newline at end of file
diff --git a/app/lib/pagination/__init__.py b/app/lib/pagination/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/lib/pagination/__init__.py
@@ -0,0 +1 @@
+
diff --git a/app/lib/pagination/middleware.py b/app/lib/pagination/middleware.py
new file mode 100644
index 0000000..cf9f9cb
--- /dev/null
+++ b/app/lib/pagination/middleware.py
@@ -0,0 +1,25 @@
+class PaginationMiddleware(object):
+ """
+ Inserts a variable representing the current page onto the request object if
+ it exists in either **GET** or **POST** portions of the request.
+ """
+ def process_request(self, request):
+ try:
+ request.page = int(request.REQUEST['page'])
+ except (KeyError, ValueError):
+ request.page = 1
+
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ if 'paginate' in view_kwargs:
+ del view_kwargs['paginate']
+
+ if 'page_url' in view_kwargs:
+ request.page_url = view_kwargs['page_url']
+ del view_kwargs['page_url']
+
+ if 'page' in view_kwargs:
+ request.page = int(view_kwargs['page'])
+ del view_kwargs['page']
+ else:
+ request.page = 1
+
diff --git a/app/lib/pagination/models.py b/app/lib/pagination/models.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/lib/pagination/models.py
@@ -0,0 +1 @@
+
diff --git a/app/lib/pagination/templates/pagination/pagination.html b/app/lib/pagination/templates/pagination/pagination.html
new file mode 100644
index 0000000..0a7f9a4
--- /dev/null
+++ b/app/lib/pagination/templates/pagination/pagination.html
@@ -0,0 +1,9 @@
+{% load pagination_tags %}{% if is_paginated %}
+ <ul class="pages">{% if page_obj.has_previous %}
+ <li><a href="{% if prev_page == 1%}/{{request.base_path}}/{%else%}{% page_path prev_page %}{%endif%}" class="prev"> Newer</a></li>{% endif %}{% for page in pages %}{% if page %}{% if page == page_obj.number %}
+ <li class="current page">{{ page }}</li>{% else %}
+ <li><a href="{% if page == 1%}/{{request.base_path}}/{%else%}{% page_path page %}{%endif%}" class="page">{{ page }}</a></li>{% endif %}{% else %}
+ <li>...</li>{% endif %}{% endfor %}{% if page_obj.has_next %}
+ <li><a href="{% if use_page_path %}{% page_path next_page %}{% else %}?page={{ page_obj.next_page_number }}{{ getvars }}{% endif %}" class="next">Older</a></li>
+ {% endif %}
+ </ul>{% endif %}
diff --git a/app/lib/pagination/templatetags/__init__.py b/app/lib/pagination/templatetags/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/lib/pagination/templatetags/__init__.py
@@ -0,0 +1 @@
+
diff --git a/app/lib/pagination/templatetags/pagination_tags.py b/app/lib/pagination/templatetags/pagination_tags.py
new file mode 100644
index 0000000..0356f1f
--- /dev/null
+++ b/app/lib/pagination/templatetags/pagination_tags.py
@@ -0,0 +1,237 @@
+try:
+ set
+except NameError:
+ from sets import Set as set
+import re
+from django import template
+from django.db.models.query import QuerySet
+from django.core.paginator import Paginator, InvalidPage
+
+register = template.Library()
+
+DEFAULT_PAGINATION = 20
+DEFAULT_WINDOW = 4
+DEFAULT_ORPHANS = 0
+
+@register.tag
+def page_path(parser, token):
+ """Returns the path for the given page."""
+ bits = token.split_contents()
+ if len(bits) != 2:
+ raise template.TemplateSyntaxError(
+ 'page_path requires a page template var'
+ )
+
+ return PagePathNode(bits[1])
+
+class PagePathNode(template.Node):
+ """Renders the path for a given page number"""
+ def __init__(self, page_var):
+ self.page_var = page_var
+
+ def render(self, context):
+ try:
+ page = int(context[self.page_var])
+ path = context['request'].path
+ page_url = context['request'].page_url
+ except:
+ return ''
+
+ return page_url % page
+
+def do_autopaginate(parser, token):
+ """
+ Splits the arguments to the autopaginate tag and formats them correctly.
+ """
+ split = token.split_contents()
+ if len(split) == 2:
+ return AutoPaginateNode(split[1])
+ elif len(split) == 3:
+ try:
+ paginate_by = int(split[2])
+ except ValueError:
+ raise template.TemplateSyntaxError('Got %s, but expected integer.' % split[2])
+ return AutoPaginateNode(split[1], paginate_by=paginate_by)
+ elif len(split) == 4:
+ try:
+ paginate_by = int(split[2])
+ except ValueError:
+ raise template.TemplateSyntaxError('Got %s, but expected integer.' % split[2])
+ try:
+ orphans = int(split[3])
+ except ValueError:
+ raise template.TemplateSyntaxError('Got %s, but expected integer.' % split[3])
+ return AutoPaginateNode(split[1], paginate_by=paginate_by, orphans=orphans)
+ else:
+ raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
+
+class AutoPaginateNode(template.Node):
+ """
+ Emits the required objects to allow for Digg-style pagination.
+
+ First, it looks in the current context for the variable specified. This
+ should be either a QuerySet or a list.
+
+ 1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a
+ ``QuerySetPaginator`` and the current page object into the context names
+ ``paginator`` and ``page_obj``, respectively.
+
+ 2. If it is a list, this ``AutoPaginateNode`` will emit a simple
+ ``Paginator`` and the current page object into the context names
+ ``paginator`` and ``page_obj``, respectively.
+
+ It will then replace the variable specified with only the objects for the
+ current page.
+
+ .. note::
+
+ It is recommended to use *{% paginate %}* after using the autopaginate
+ tag. If you choose not to use *{% paginate %}*, make sure to display the
+ list of availabale pages, or else the application may seem to be buggy.
+ """
+ def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION, orphans=DEFAULT_ORPHANS):
+ self.queryset_var = template.Variable(queryset_var)
+ self.paginate_by = paginate_by
+ self.orphans = orphans
+
+ def render(self, context):
+ key = self.queryset_var.var
+ value = self.queryset_var.resolve(context)
+ if issubclass(value.__class__, QuerySet):
+ model = value.model
+ paginator_class = Paginator
+ else:
+ value = list(value)
+ try:
+ model = value[0].__class__
+ except IndexError:
+ return ''
+ paginator_class = Paginator
+ paginator = paginator_class(value, self.paginate_by, self.orphans)
+ try:
+ page_obj = paginator.page(context['request'].page)
+ except InvalidPage:
+ context[key] = []
+ context['invalid_page'] = True
+ return ''
+ context[key] = page_obj.object_list
+ context['paginator'] = paginator
+ context['page_obj'] = page_obj
+ if hasattr(context['request'], 'page_url'):
+ context['use_page_path'] = True
+ return ''
+
+def paginate(context, window=DEFAULT_WINDOW):
+ """
+ Renders the ``pagination/pagination.html`` template, resulting in a
+ Digg-like display of the available pages, given the current page. If there
+ are too many pages to be displayed before and after the current page, then
+ elipses will be used to indicate the undisplayed gap between page numbers.
+
+ Requires one argument, ``context``, which should be a dictionary-like data
+ structure and must contain the following keys:
+
+ ``paginator``
+ A ``Paginator`` or ``QuerySetPaginator`` object.
+
+ ``page_obj``
+ This should be the result of calling the page method on the
+ aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
+ the current page.
+
+ This same ``context`` dictionary-like data structure may also include:
+
+ ``getvars``
+ A dictionary of all of the **GET** parameters in the current request.
+ This is useful to maintain certain types of state, even when requesting
+ a different page.
+ """
+ try:
+ paginator = context['paginator']
+ page_obj = context['page_obj']
+ page_range = paginator.page_range
+ # First and last are simply the first *n* pages and the last *n* pages,
+ # where *n* is the current window size.
+ first = set(page_range[:window])
+ last = set(page_range[-window:])
+ # Now we look around our current page, making sure that we don't wrap
+ # around.
+ current_start = page_obj.number-1-window
+ if current_start < 0:
+ current_start = 0
+ current_end = page_obj.number-1+window
+ if current_end < 0:
+ current_end = 0
+ current = set(page_range[current_start:current_end])
+ pages = []
+ # If there's no overlap between the first set of pages and the current
+ # set of pages, then there's a possible need for elusion.
+ if len(first.intersection(current)) == 0:
+ first_list = sorted(list(first))
+ second_list = sorted(list(current))
+ pages.extend(first_list)
+ diff = second_list[0] - first_list[-1]
+ # If there is a gap of two, between the last page of the first
+ # set and the first page of the current set, then we're missing a
+ # page.
+ if diff == 2:
+ pages.append(second_list[0] - 1)
+ # If the difference is just one, then there's nothing to be done,
+ # as the pages need no elusion and are correct.
+ elif diff == 1:
+ pass
+ # Otherwise, there's a bigger gap which needs to be signaled for
+ # elusion, by pushing a None value to the page list.
+ else:
+ pages.append(None)
+ pages.extend(second_list)
+ else:
+ pages.extend(sorted(list(first.union(current))))
+ # If there's no overlap between the current set of pages and the last
+ # set of pages, then there's a possible need for elusion.
+ if len(current.intersection(last)) == 0:
+ second_list = sorted(list(last))
+ diff = second_list[0] - pages[-1]
+ # If there is a gap of two, between the last page of the current
+ # set and the first page of the last set, then we're missing a
+ # page.
+ if diff == 2:
+ pages.append(second_list[0] - 1)
+ # If the difference is just one, then there's nothing to be done,
+ # as the pages need no elusion and are correct.
+ elif diff == 1:
+ pass
+ # Otherwise, there's a bigger gap which needs to be signaled for
+ # elusion, by pushing a None value to the page list.
+ else:
+ pages.append(None)
+ pages.extend(second_list)
+ else:
+ pages.extend(sorted(list(last.difference(current))))
+ to_return = {
+ 'pages': pages,
+ 'page_obj': page_obj,
+ 'paginator': paginator,
+ 'is_paginated': paginator.count > paginator.per_page,
+ }
+ if 'request' in context:
+ if 'use_page_path' in context:
+ to_return['request'] = context['request']
+ to_return['use_page_path'] = context['use_page_path']
+ if page_obj.has_previous():
+ to_return['prev_page'] = page_obj.previous_page_number()
+ if page_obj.has_next():
+ to_return['next_page'] = page_obj.next_page_number()
+
+ getvars = context['request'].GET.copy()
+ if 'page' in getvars:
+ del getvars['page']
+ if len(getvars.keys()) > 0:
+ to_return['getvars'] = "&%s" % getvars.urlencode()
+ else:
+ to_return['getvars'] = ''
+ return to_return
+ except KeyError:
+ return {}
+register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
+register.tag('autopaginate', do_autopaginate)
diff --git a/app/lib/pagination/tests.py b/app/lib/pagination/tests.py
new file mode 100644
index 0000000..ba55c03
--- /dev/null
+++ b/app/lib/pagination/tests.py
@@ -0,0 +1,52 @@
+"""
+>>> from django.core.paginator import Paginator
+>>> from pagination.templatetags.pagination_tags import paginate
+>>> from django.template import Template, Context
+
+>>> p = Paginator(range(15), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, 5, 6, 7, 8]
+
+>>> p = Paginator(range(17), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+
+>>> p = Paginator(range(19), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 7, 8, 9, 10]
+
+>>> p = Paginator(range(21), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 8, 9, 10, 11]
+
+# Testing orphans
+>>> p = Paginator(range(5), 2, 1)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2]
+
+>>> p = Paginator(range(21), 2, 1)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 7, 8, 9, 10]
+
+>>> t = Template("{% load pagination_tags %}{% autopaginate var 2 %}{% paginate %}")
+
+# WARNING: Please, please nobody read this portion of the code!
+>>> class GetProxy(object):
+... def __iter__(self): yield self.__dict__.__iter__
+... def copy(self): return self
+... def urlencode(self): return u''
+... def keys(self): return []
+>>> class RequestProxy(object):
+... page = 1
+... GET = GetProxy()
+>>>
+# ENDWARNING
+
+>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
+u'\\n<div class="pagination">...
+>>>
+>>> t = Template("{% load pagination_tags %}{% autopaginate var %}{% paginate %}")
+>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
+u'\\n<div class="pagination">...
+>>>
+"""
diff --git a/app/lib/templatetags/__init__.py b/app/lib/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/templatetags/__init__.py
diff --git a/app/lib/templatetags/templatetags/__init__.py b/app/lib/templatetags/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/templatetags/templatetags/__init__.py
diff --git a/app/lib/templatetags/templatetags/admin_reorder.py b/app/lib/templatetags/templatetags/admin_reorder.py
new file mode 100644
index 0000000..83ccf9e
--- /dev/null
+++ b/app/lib/templatetags/templatetags/admin_reorder.py
@@ -0,0 +1,45 @@
+from django.template.base import Node
+from django.conf import settings
+from django import template
+
+# Get template.Library instance
+register = template.Library()
+
+
+class AppOrderNode(Node):
+ """
+ Reorders the app_list and child model lists on the admin index page.
+ """
+ def render(self, context):
+ if 'app_list' in context:
+ app_list = list(context['app_list'])
+ ordered = []
+ # look at each app in the user order
+ for app in settings.ADMIN_REORDER:
+ app_name, app_models = app[0], app[1]
+ # look at each app in the orig order
+ for app_def in app_list:
+ if app_def['name'] == app_name:
+ model_list = list(app_def['models'])
+ mord = []
+ # look at models in user order
+ for model_name in app_models:
+ # look at models in orig order
+ for model_def in model_list:
+ if model_def['name'] == model_name:
+ mord.append(model_def)
+ model_list.remove(model_def)
+ break
+ mord[len(mord):] = model_list
+ ordered.append({'app_url': app_def['app_url'],
+ 'models': mord, 'name': app_def['name']})
+ app_list.remove(app_def)
+ break
+ ordered[len(ordered):] = app_list
+ context['app_list'] = ordered
+ return ''
+
+
+def app_order(parser, token):
+ return AppOrderNode()
+var = register.tag(app_order)
diff --git a/app/lib/templatetags/templatetags/amp.py b/app/lib/templatetags/templatetags/amp.py
new file mode 100644
index 0000000..d539de7
--- /dev/null
+++ b/app/lib/templatetags/templatetags/amp.py
@@ -0,0 +1,41 @@
+from django import template
+from PIL import Image
+from io import BytesIO
+try:
+ import Image
+ import ImageFile
+except ImportError:
+ try:
+ from PIL import Image
+ from PIL import ImageFile
+ except ImportError:
+ raise ImportError("Could not import the Python Imaging Library.")
+
+import requests
+from bs4 import BeautifulSoup
+from builder.sanitizer import Sanitizer
+
+register = template.Library()
+
+
+def remove_img_tags(text):
+ soup = BeautifulSoup(text, 'lxml')
+ for img in soup.find_all('img'):
+ r = requests.get(img['src'])
+ i = Image.open(BytesIO(r.content))
+ width, height = i.size
+ try:
+ new_tag = soup.new_tag("amp-img", alt=img["alt"], width=width, height=height, src=img['src'], srcset=img['srcset'])
+ except:
+ new_tag = soup.new_tag("amp-img", alt=img["alt"], width=width, height=height, src=img['src'])
+ img.replace_with(new_tag)
+ soup = soup.body.encode_contents()
+ return soup
+
+
+def do_amp(text):
+ bs = Sanitizer().strip(text)
+ amp = remove_img_tags(bs)
+ return amp
+
+register.filter('amp', do_amp)
diff --git a/app/lib/templatetags/templatetags/expense_total.py b/app/lib/templatetags/templatetags/expense_total.py
new file mode 100644
index 0000000..8bf0953
--- /dev/null
+++ b/app/lib/templatetags/templatetags/expense_total.py
@@ -0,0 +1,25 @@
+from decimal import Decimal
+from django import template
+from django.utils.safestring import mark_safe
+register = template.Library()
+
+@register.filter
+def expense_total(values):
+ """
+ converts spaces to hyphens.
+ """
+ total = 0
+ for items in values:
+ for item in items['list']:
+ total += Decimal(item.amount)
+ return mark_safe(total)
+
+@register.filter
+def cat_total(values):
+ """
+ converts spaces to hyphens.
+ """
+ total = 0
+ for val in values:
+ total += Decimal(val.amount)
+ return mark_safe(total)
diff --git a/app/lib/templatetags/templatetags/f_to_c.py b/app/lib/templatetags/templatetags/f_to_c.py
new file mode 100644
index 0000000..343395a
--- /dev/null
+++ b/app/lib/templatetags/templatetags/f_to_c.py
@@ -0,0 +1,7 @@
+from django import template
+register = template.Library()
+
+
+@register.filter
+def f_to_c(temp):
+ return str(round((int(temp) - 32) * 5.0/9.0, 1))
diff --git a/app/lib/templatetags/templatetags/get_next.py b/app/lib/templatetags/templatetags/get_next.py
new file mode 100644
index 0000000..97159f5
--- /dev/null
+++ b/app/lib/templatetags/templatetags/get_next.py
@@ -0,0 +1,25 @@
+from django import template
+
+register = template.Library()
+
+@register.filter
+def next(some_list, current_index):
+ """
+ Returns the next element of the list using the current index if it exists.
+ Otherwise returns an empty string.
+ """
+ try:
+ return some_list[int(current_index) + 1] # access the next element
+ except:
+ return '' # return empty string in case of exception
+
+@register.filter
+def previous(some_list, current_index):
+ """
+ Returns the previous element of the list using the current index if it exists.
+ Otherwise returns an empty string.
+ """
+ try:
+ return some_list[int(current_index) - 1] # access the previous element
+ except:
+ return '' # return empty string in case of exception
diff --git a/app/lib/templatetags/templatetags/gravatar_local.py b/app/lib/templatetags/templatetags/gravatar_local.py
new file mode 100644
index 0000000..5ed9a7f
--- /dev/null
+++ b/app/lib/templatetags/templatetags/gravatar_local.py
@@ -0,0 +1,27 @@
+from django import template
+from django.utils.html import escape
+
+from django_gravatar.helpers import calculate_gravatar_hash, has_gravatar
+
+# Get template.Library instance
+register = template.Library()
+
+def gravatar_hash(user_or_email):
+ if hasattr(user_or_email, 'email'):
+ email = user_or_email.email
+ else:
+ email = user_or_email
+ try:
+ #return get_gravatar_profile_url(email)
+ return calculate_gravatar_hash(email)
+ except:
+ return ''
+
+@register.filter
+def has_grav(email):
+ if has_gravatar(email):
+ return True
+ else:
+ return False
+
+register.simple_tag(gravatar_hash)
diff --git a/app/lib/templatetags/templatetags/html5_datetime.py b/app/lib/templatetags/templatetags/html5_datetime.py
new file mode 100644
index 0000000..20b5f27
--- /dev/null
+++ b/app/lib/templatetags/templatetags/html5_datetime.py
@@ -0,0 +1,16 @@
+from django.utils import dateformat
+import datetime
+from django import template
+from django.utils.safestring import mark_safe
+register = template.Library()
+
+@register.filter
+def html5_datetime(value):
+ """
+ Adds to colons to django's "Z" format to match html5 guidelines
+ """
+ fmt = "Y-m-d\TH:i:sO"
+ value = value
+ str = dateformat.format(value, fmt)
+ s = str[:-2] + ':' + str[-2:]
+ return mark_safe(s)
diff --git a/app/lib/templatetags/templatetags/markdown.py b/app/lib/templatetags/templatetags/markdown.py
new file mode 100644
index 0000000..f14d5e0
--- /dev/null
+++ b/app/lib/templatetags/templatetags/markdown.py
@@ -0,0 +1,10 @@
+from django import template
+import markdown
+
+register = template.Library()
+
+def do_markdown(text):
+
+ return markdown.markdown(text, extensions=['extra'], safe_mode=False)
+
+register.filter('markdown', do_markdown)
diff --git a/app/lib/templatetags/templatetags/month_number_to_name.py b/app/lib/templatetags/templatetags/month_number_to_name.py
new file mode 100644
index 0000000..69bd7e6
--- /dev/null
+++ b/app/lib/templatetags/templatetags/month_number_to_name.py
@@ -0,0 +1,7 @@
+import calendar
+from django import template
+register = template.Library()
+
+@register.filter
+def month_number_to_name(month_number):
+ return calendar.month_name[int(month_number)]
diff --git a/app/lib/templatetags/templatetags/nofollow.py b/app/lib/templatetags/templatetags/nofollow.py
new file mode 100644
index 0000000..85fc166
--- /dev/null
+++ b/app/lib/templatetags/templatetags/nofollow.py
@@ -0,0 +1,15 @@
+from django.template import Library
+import re
+
+register = Library()
+
+r_nofollow = re.compile('<a (?![^>]*nofollow)')
+s_nofollow = '<a rel="nofollow" '
+
+def nofollow(value):
+ return r_nofollow.sub(s_nofollow, value)
+
+register.filter(nofollow)
+
+
+re.compile('<a (?![^>]*rel=["\']nofollow[\'"])')
diff --git a/app/lib/templatetags/templatetags/number_to_word.py b/app/lib/templatetags/templatetags/number_to_word.py
new file mode 100644
index 0000000..c153932
--- /dev/null
+++ b/app/lib/templatetags/templatetags/number_to_word.py
@@ -0,0 +1,29 @@
+from django.utils.translation import ungettext, ugettext as _
+import re
+from django import template
+from django.utils.safestring import mark_safe
+register = template.Library()
+
+
+
+SPECIAL_CASES = (_('ten'), _('eleven'), _('twelve'), _('thirteen'), _('fourteen'), _('fifteen'), _('sixteen'), _('seventeen'), _('eighteen'), _('nineteen'),)
+BASE_INT = (_('twenty'), _('thirty'), _('forty'), _('fifty'), _('sixty'), _('seventy'), _('eighty'), _('ninety'))
+PRIME_NUM = (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'),)
+
+@register.filter
+def number_to_word(value):
+ """
+ For numbers 1-99, returns the number spelled out.
+ """
+ value = str(value)
+ if len(value) == 2:
+ if int(value[:1]) == 1:
+ word = SPECIAL_CASES[int(value[1:])]
+ return word
+ else:
+ word = BASE_INT[int(value[:1])-2]
+ word = word + '-'+str(PRIME_NUM[int(value[1:])-1])
+ else:
+ word = PRIME_NUM[int(value[:1])-1]
+ return word
+ \ No newline at end of file
diff --git a/app/lib/templatetags/templatetags/slugify_under.py b/app/lib/templatetags/templatetags/slugify_under.py
new file mode 100644
index 0000000..bbf01d2
--- /dev/null
+++ b/app/lib/templatetags/templatetags/slugify_under.py
@@ -0,0 +1,15 @@
+import re
+from django import template
+from django.utils.safestring import mark_safe
+register = template.Library()
+
+@register.filter
+def slugify_under(value):
+ """
+ Normalizes string, converts to lowercase, removes non-alpha characters,
+ and converts spaces to hyphens.
+ """
+ import unicodedata
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
+ value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
+ return mark_safe(re.sub('[-\s]+', '_', value))
diff --git a/app/lib/templatetags/templatetags/truncateletters.py b/app/lib/templatetags/templatetags/truncateletters.py
new file mode 100644
index 0000000..c492430
--- /dev/null
+++ b/app/lib/templatetags/templatetags/truncateletters.py
@@ -0,0 +1,24 @@
+from django import template
+register = template.Library()
+
+@register.filter
+def truncateletters(value, arg):
+ """
+ Truncates a string after a certain number of letters
+
+ Argument: Number of letters to truncate after
+ """
+ try:
+ length = int(arg)
+ except ValueError: # invalid literal for int()
+ return value # Fail silently
+ if not isinstance(value, basestring):
+ value = str(value)
+
+ if len(value) > length:
+ truncated = value[:length]
+ if not truncated.endswith('...'):
+ truncated += '...'
+ return truncated
+
+ return value \ No newline at end of file
diff --git a/app/lib/upload/__init__.py b/app/lib/upload/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/upload/__init__.py
diff --git a/app/lib/upload/admin.py b/app/lib/upload/admin.py
new file mode 100644
index 0000000..9d8a2b2
--- /dev/null
+++ b/app/lib/upload/admin.py
@@ -0,0 +1,50 @@
+from django.contrib import admin
+from django.utils.translation import ugettext as _
+from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.utils.encoding import force_unicode
+from django.utils.html import escape
+from upload.models import FileUpload
+from django.conf import settings
+
+
+class UploadAdmin(admin.ModelAdmin):
+ list_display = ('title','upload_date','upload', 'content_type')
+ search_fields = ['title',]
+ list_filter = ('upload_date', 'content_type')
+ fieldsets = (
+ (None, {'fields': ('upload','title','description')}),
+ )
+ class Media:
+ js = ['%s/admin/jquery/jquery-1.3.2.min.js' % (settings.MEDIA_URL), '%s/admin/custom/photo-edit.js' % (settings.MEDIA_URL)]
+
+ def response_change(self, request, obj):
+ """
+ Determines the HttpResponse for the change_view stage.
+ """
+ opts = obj._meta
+ pk_value = obj._get_pk_val()
+
+ msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
+ if request.POST.has_key("_continue"):
+ self.message_user(request, msg + ' ' + _("You may edit it again below."))
+ if request.REQUEST.has_key('_popup'):
+ return HttpResponseRedirect(request.path + "?_popup=1")
+ else:
+ return HttpResponseRedirect(request.path)
+ elif request.POST.has_key("_saveasnew"):
+ msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj}
+ self.message_user(request, msg)
+ return HttpResponseRedirect("../%s/" % pk_value)
+ elif request.POST.has_key("_addanother"):
+ self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
+ return HttpResponseRedirect("../add/")
+ elif request.POST.has_key("_popup"):
+ return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
+ # escape() calls force_unicode.
+ (escape(pk_value), escape(obj)))
+ else:
+ self.message_user(request, msg)
+ return HttpResponseRedirect("../")
+
+
+admin.site.register(FileUpload, UploadAdmin) \ No newline at end of file
diff --git a/app/lib/upload/flickr.py b/app/lib/upload/flickr.py
new file mode 100644
index 0000000..a4cd224
--- /dev/null
+++ b/app/lib/upload/flickr.py
@@ -0,0 +1,889 @@
+"""
+ flickr.py
+ Copyright 2004-6 James Clarke <james@jamesclarke.info>
+
+THIS SOFTWARE IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, AND MAY BE
+COPIED, MODIFIED OR DISTRIBUTED IN ANY WAY, AS LONG AS THIS NOTICE
+AND ACKNOWLEDGEMENT OF AUTHORSHIP REMAIN.
+
+2006-12-19
+Applied patches from Berco Beute and Wolfram Kriesing.
+TODO list below is out of date!
+
+2005-06-10
+TOOD list:
+* flickr.blogs.*
+* flickr.contacts.getList
+* flickr.groups.browse
+* flickr.groups.getActiveList
+* flickr.people.getOnlineList
+* flickr.photos.getContactsPhotos
+* flickr.photos.getContactsPublicPhotos
+* flickr.photos.getContext
+* flickr.photos.getCounts
+* flickr.photos.getExif
+* flickr.photos.getNotInSet
+* flickr.photos.getPerms
+* flickr.photos.getRecent
+* flickr.photos.getUntagged
+* flickr.photos.setDates
+* flickr.photos.setPerms
+* flickr.photos.licenses.*
+* flickr.photos.notes.*
+* flickr.photos.transform.*
+* flickr.photosets.getContext
+* flickr.photosets.orderSets
+* flickr.reflection.* (not important)
+* flickr.tags.getListPhoto
+* flickr.urls.*
+"""
+
+__author__ = "James Clarke <james@jamesclarke.info>"
+__version__ = "$Rev$"
+__date__ = "$Date$"
+__copyright__ = "Copyright 2004-6 James Clarke"
+
+from urllib import urlencode, urlopen
+from xml.dom import minidom
+
+HOST = 'http://flickr.com'
+API = '/services/rest'
+
+#set these here or using flickr.API_KEY in your application
+API_KEY = ''
+email = None
+password = None
+
+class FlickrError(Exception): pass
+
+class Photo(object):
+ """Represents a Flickr Photo."""
+
+ __readonly = ['id', 'secret', 'server', 'isfavorite', 'license', 'rotation',
+ 'owner', 'dateposted', 'datetaken', 'takengranularity',
+ 'title', 'description', 'ispublic', 'isfriend', 'isfamily',
+ 'cancomment', 'canaddmeta', 'comments', 'tags', 'permcomment',
+ 'permaddmeta']
+
+ #XXX: Hopefully None won't cause problems
+ def __init__(self, id, owner=None, dateuploaded=None, \
+ title=None, description=None, ispublic=None, \
+ isfriend=None, isfamily=None, cancomment=None, \
+ canaddmeta=None, comments=None, tags=None, secret=None, \
+ isfavorite=None, server=None, license=None, rotation=None):
+ """Must specify id, rest is optional."""
+ self.__loaded = False
+ self.__cancomment = cancomment
+ self.__canaddmeta = canaddmeta
+ self.__comments = comments
+ self.__dateuploaded = dateuploaded
+ self.__description = description
+ self.__id = id
+ self.__license = license
+ self.__isfamily = isfamily
+ self.__isfavorite = isfavorite
+ self.__isfriend = isfriend
+ self.__ispublic = ispublic
+ self.__owner = owner
+ self.__rotation = rotation
+ self.__secret = secret
+ self.__server = server
+ self.__tags = tags
+ self.__title = title
+
+ self.__dateposted = None
+ self.__datetaken = None
+ self.__takengranularity = None
+ self.__permcomment = None
+ self.__permaddmeta = None
+
+ def __setattr__(self, key, value):
+ if key in self.__class__.__readonly:
+ raise AttributeError("The attribute %s is read-only." % key)
+ else:
+ super(Photo, self).__setattr__(key, value)
+
+ def __getattr__(self, key):
+ if not self.__loaded:
+ self._load_properties()
+ if key in self.__class__.__readonly:
+ return super(Photo, self).__getattribute__("_%s__%s" % (self.__class__.__name__, key))
+ else:
+ return super(Photo, self).__getattribute__(key)
+
+ def _load_properties(self):
+ """Loads the properties from Flickr."""
+ self.__loaded = True
+
+ method = 'flickr.photos.getInfo'
+ data = _doget(method, photo_id=self.id)
+
+ photo = data.rsp.photo
+
+ self.__secret = photo.secret
+ self.__server = photo.server
+ self.__isfavorite = photo.isfavorite
+ self.__license = photo.license
+ self.__rotation = photo.rotation
+
+
+
+ owner = photo.owner
+ self.__owner = User(owner.nsid, username=owner.username,\
+ realname=owner.realname,\
+ location=owner.location)
+
+ self.__title = photo.title.text
+ self.__description = photo.description.text
+ self.__ispublic = photo.visibility.ispublic
+ self.__isfriend = photo.visibility.isfriend
+ self.__isfamily = photo.visibility.isfamily
+
+ self.__dateposted = photo.dates.posted
+ self.__datetaken = photo.dates.taken
+ self.__takengranularity = photo.dates.takengranularity
+
+ self.__cancomment = photo.editability.cancomment
+ self.__canaddmeta = photo.editability.canaddmeta
+ self.__comments = photo.comments.text
+
+ try:
+ self.__permcomment = photo.permissions.permcomment
+ self.__permaddmeta = photo.permissions.permaddmeta
+ except AttributeError:
+ self.__permcomment = None
+ self.__permaddmeta = None
+
+ #TODO: Implement Notes?
+ if hasattr(photo.tags, "tag"):
+ if isinstance(photo.tags.tag, list):
+ self.__tags = [Tag(tag.id, User(tag.author), tag.raw, tag.text) \
+ for tag in photo.tags.tag]
+ else:
+ tag = photo.tags.tag
+ self.__tags = [Tag(tag.id, User(tag.author), tag.raw, tag.text)]
+
+
+ def __str__(self):
+ return '<Flickr Photo %s>' % self.id
+
+
+ def setTags(self, tags):
+ """Set the tags for current photo to list tags.
+ (flickr.photos.settags)
+ """
+ method = 'flickr.photos.setTags'
+ tags = uniq(tags)
+ _dopost(method, auth=True, photo_id=self.id, tags=tags)
+ self._load_properties()
+
+
+ def addTags(self, tags):
+ """Adds the list of tags to current tags. (flickr.photos.addtags)
+ """
+ method = 'flickr.photos.addTags'
+ if isinstance(tags, list):
+ tags = uniq(tags)
+
+ _dopost(method, auth=True, photo_id=self.id, tags=tags)
+ #load properties again
+ self._load_properties()
+
+ def removeTag(self, tag):
+ """Remove the tag from the photo must be a Tag object.
+ (flickr.photos.removeTag)
+ """
+ method = 'flickr.photos.removeTag'
+ tag_id = ''
+ try:
+ tag_id = tag.id
+ except AttributeError:
+ raise FlickrError, "Tag object expected"
+ _dopost(method, auth=True, photo_id=self.id, tag_id=tag_id)
+ self._load_properties()
+
+
+ def setMeta(self, title=None, description=None):
+ """Set metadata for photo. (flickr.photos.setMeta)"""
+ method = 'flickr.photos.setMeta'
+
+ if title is None:
+ title = self.title
+ if description is None:
+ description = self.description
+
+ _dopost(method, auth=True, title=title, \
+ description=description, photo_id=self.id)
+
+ self.__title = title
+ self.__description = description
+
+
+ def getURL(self, size='Medium', urlType='url'):
+ """Retrieves a url for the photo. (flickr.photos.getSizes)
+
+ urlType - 'url' or 'source'
+ 'url' - flickr page of photo
+ 'source' - image file
+ """
+ method = 'flickr.photos.getSizes'
+ data = _doget(method, photo_id=self.id)
+ for psize in data.rsp.sizes.size:
+ if psize.label == size:
+ return getattr(psize, urlType)
+ raise FlickrError, "No URL found"
+
+ def getSizes(self):
+ """
+ Get all the available sizes of the current image, and all available
+ data about them.
+ Returns: A list of dicts with the size data.
+ """
+ method = 'flickr.photos.getSizes'
+ data = _doget(method, photo_id=self.id)
+ ret = []
+ # The given props are those that we return and the according types, since
+ # return width and height as string would make "75">"100" be True, which
+ # is just error prone.
+ props = {'url':str,'width':int,'height':int,'label':str,'source':str,'text':str}
+ for psize in data.rsp.sizes.size:
+ d = {}
+ for prop,convert_to_type in props.items():
+ d[prop] = convert_to_type(getattr(psize, prop))
+ ret.append(d)
+ return ret
+
+ #def getExif(self):
+ #method = 'flickr.photos.getExif'
+ #data = _doget(method, photo_id=self.id)
+ #ret = []
+ #for exif in data.rsp.photo.exif:
+ #print exif.label, dir(exif)
+ ##ret.append({exif.label:exif.})
+ #return ret
+ ##raise FlickrError, "No URL found"
+
+ def getLocation(self):
+ """
+ Return the latitude+longitutde of the picture.
+ Returns None if no location given for this pic.
+ """
+ method = 'flickr.photos.geo.getLocation'
+ try:
+ data = _doget(method, photo_id=self.id)
+ except FlickrError: # Some other error might have occured too!?
+ return None
+ loc = data.rsp.photo.location
+ return [loc.latitude, loc.longitude]
+
+
+class Photoset(object):
+ """A Flickr photoset."""
+
+ def __init__(self, id, title, primary, photos=0, description='', \
+ secret='', server=''):
+ self.__id = id
+ self.__title = title
+ self.__primary = primary
+ self.__description = description
+ self.__count = photos
+ self.__secret = secret
+ self.__server = server
+
+ id = property(lambda self: self.__id)
+ title = property(lambda self: self.__title)
+ description = property(lambda self: self.__description)
+ primary = property(lambda self: self.__primary)
+
+ def __len__(self):
+ return self.__count
+
+ def __str__(self):
+ return '<Flickr Photoset %s>' % self.id
+
+ def getPhotos(self):
+ """Returns list of Photos."""
+ method = 'flickr.photosets.getPhotos'
+ data = _doget(method, photoset_id=self.id)
+ photos = data.rsp.photoset.photo
+ p = []
+ for photo in photos:
+ p.append(Photo(photo.id, title=photo.title, secret=photo.secret, \
+ server=photo.server))
+ return p
+
+ def editPhotos(self, photos, primary=None):
+ """Edit the photos in this set.
+
+ photos - photos for set
+ primary - primary photo (if None will used current)
+ """
+ method = 'flickr.photosets.editPhotos'
+
+ if primary is None:
+ primary = self.primary
+
+ ids = [photo.id for photo in photos]
+ if primary.id not in ids:
+ ids.append(primary.id)
+
+ _dopost(method, auth=True, photoset_id=self.id,\
+ primary_photo_id=primary.id,
+ photo_ids=ids)
+ self.__count = len(ids)
+ return True
+
+ def addPhoto(self, photo):
+ """Add a photo to this set.
+
+ photo - the photo
+ """
+ method = 'flickr.photosets.addPhoto'
+
+ _dopost(method, auth=True, photoset_id=self.id, photo_id=photo.id)
+
+ self.__count += 1
+ return True
+
+ def removePhoto(self, photo):
+ """Remove the photo from this set.
+
+ photo - the photo
+ """
+ method = 'flickr.photosets.removePhoto'
+
+ _dopost(method, auth=True, photoset_id=self.id, photo_id=photo.id)
+ self.__count = self.__count - 1
+ return True
+
+ def editMeta(self, title=None, description=None):
+ """Set metadata for photo. (flickr.photos.setMeta)"""
+ method = 'flickr.photosets.editMeta'
+
+ if title is None:
+ title = self.title
+ if description is None:
+ description = self.description
+
+ _dopost(method, auth=True, title=title, \
+ description=description, photoset_id=self.id)
+
+ self.__title = title
+ self.__description = description
+ return True
+
+ #XXX: Delete isn't handled well as the python object will still exist
+ def delete(self):
+ """Deletes the photoset.
+ """
+ method = 'flickr.photosets.delete'
+
+ _dopost(method, auth=True, photoset_id=self.id)
+ return True
+
+ def create(cls, photo, title, description=''):
+ """Create a new photoset.
+
+ photo - primary photo
+ """
+ if not isinstance(photo, Photo):
+ raise TypeError, "Photo expected"
+
+ method = 'flickr.photosets.create'
+ data = _dopost(method, auth=True, title=title,\
+ description=description,\
+ primary_photo_id=photo.id)
+
+ set = Photoset(data.rsp.photoset.id, title, Photo(photo.id),
+ photos=1, description=description)
+ return set
+ create = classmethod(create)
+
+
+class User(object):
+ """A Flickr user."""
+
+ def __init__(self, id, username=None, isadmin=None, ispro=None, \
+ realname=None, location=None, firstdate=None, count=None):
+ """id required, rest optional."""
+ self.__loaded = False #so we don't keep loading data
+ self.__id = id
+ self.__username = username
+ self.__isadmin = isadmin
+ self.__ispro = ispro
+ self.__realname = realname
+ self.__location = location
+ self.__photos_firstdate = firstdate
+ self.__photos_count = count
+
+ #property fu
+ id = property(lambda self: self._general_getattr('id'))
+ username = property(lambda self: self._general_getattr('username'))
+ isadmin = property(lambda self: self._general_getattr('isadmin'))
+ ispro = property(lambda self: self._general_getattr('ispro'))
+ realname = property(lambda self: self._general_getattr('realname'))
+ location = property(lambda self: self._general_getattr('location'))
+ photos_firstdate = property(lambda self: \
+ self._general_getattr('photos_firstdate'))
+ photos_firstdatetaken = property(lambda self: \
+ self._general_getattr\
+ ('photos_firstdatetaken'))
+ photos_count = property(lambda self: \
+ self._general_getattr('photos_count'))
+ icon_server= property(lambda self: self._general_getattr('icon_server'))
+ icon_url= property(lambda self: self._general_getattr('icon_url'))
+
+ def _general_getattr(self, var):
+ """Generic get attribute function."""
+ if getattr(self, "_%s__%s" % (self.__class__.__name__, var)) is None \
+ and not self.__loaded:
+ self._load_properties()
+ return getattr(self, "_%s__%s" % (self.__class__.__name__, var))
+
+ def _load_properties(self):
+ """Load User properties from Flickr."""
+ method = 'flickr.people.getInfo'
+ data = _doget(method, user_id=self.__id)
+
+ self.__loaded = True
+
+ person = data.rsp.person
+
+ self.__isadmin = person.isadmin
+ self.__ispro = person.ispro
+ self.__icon_server = person.iconserver
+ if int(person.iconserver) > 0:
+ self.__icon_url = 'http://photos%s.flickr.com/buddyicons/%s.jpg' \
+ % (person.iconserver, self.__id)
+ else:
+ self.__icon_url = 'http://www.flickr.com/images/buddyicon.jpg'
+
+ self.__username = person.username.text
+ self.__realname = person.realname.text
+ self.__location = person.location.text
+ self.__photos_firstdate = person.photos.firstdate.text
+ self.__photos_firstdatetaken = person.photos.firstdatetaken.text
+ self.__photos_count = person.photos.count.text
+
+ def __str__(self):
+ return '<Flickr User %s>' % self.id
+
+ def getPhotosets(self):
+ """Returns a list of Photosets."""
+ method = 'flickr.photosets.getList'
+ data = _doget(method, user_id=self.id)
+ sets = []
+ if isinstance(data.rsp.photosets.photoset, list):
+ for photoset in data.rsp.photosets.photoset:
+ sets.append(Photoset(photoset.id, photoset.title.text,\
+ Photo(photoset.primary),\
+ secret=photoset.secret, \
+ server=photoset.server, \
+ description=photoset.description.text,
+ photos=photoset.photos))
+ else:
+ photoset = data.rsp.photosets.photoset
+ sets.append(Photoset(photoset.id, photoset.title.text,\
+ Photo(photoset.primary),\
+ secret=photoset.secret, \
+ server=photoset.server, \
+ description=photoset.description.text,
+ photos=photoset.photos))
+ return sets
+
+ def getPublicFavorites(self, per_page='', page=''):
+ return favorites_getPublicList(user_id=self.id, per_page=per_page, \
+ page=page)
+
+ def getFavorites(self, per_page='', page=''):
+ return favorites_getList(user_id=self.id, per_page=per_page, \
+ page=page)
+
+class Group(object):
+ """Flickr Group Pool"""
+ def __init__(self, id, name=None, members=None, online=None,\
+ privacy=None, chatid=None, chatcount=None):
+ self.__loaded = False
+ self.__id = id
+ self.__name = name
+ self.__members = members
+ self.__online = online
+ self.__privacy = privacy
+ self.__chatid = chatid
+ self.__chatcount = chatcount
+ self.__url = None
+
+ id = property(lambda self: self._general_getattr('id'))
+ name = property(lambda self: self._general_getattr('name'))
+ members = property(lambda self: self._general_getattr('members'))
+ online = property(lambda self: self._general_getattr('online'))
+ privacy = property(lambda self: self._general_getattr('privacy'))
+ chatid = property(lambda self: self._general_getattr('chatid'))
+ chatcount = property(lambda self: self._general_getattr('chatcount'))
+
+ def _general_getattr(self, var):
+ """Generic get attribute function."""
+ if getattr(self, "_%s__%s" % (self.__class__.__name__, var)) is None \
+ and not self.__loaded:
+ self._load_properties()
+ return getattr(self, "_%s__%s" % (self.__class__.__name__, var))
+
+ def _load_properties(self):
+ """Loads the properties from Flickr."""
+ method = 'flickr.groups.getInfo'
+ data = _doget(method, group_id=self.id)
+
+ self.__loaded = True
+
+ group = data.rsp.group
+
+ self.__name = photo.name.text
+ self.__members = photo.members.text
+ self.__online = photo.online.text
+ self.__privacy = photo.privacy.text
+ self.__chatid = photo.chatid.text
+ self.__chatcount = photo.chatcount.text
+
+ def __str__(self):
+ return '<Flickr Group %s>' % self.id
+
+ def getPhotos(self, tags='', per_page='', page=''):
+ """Get a list of photo objects for this group"""
+ method = 'flickr.groups.pools.getPhotos'
+ data = _doget(method, group_id=self.id, tags=tags,\
+ per_page=per_page, page=page)
+ photos = []
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ return photos
+
+ def add(self, photo):
+ """Adds a Photo to the group"""
+ method = 'flickr.groups.pools.add'
+ _dopost(method, auth=True, photo_id=photo.id, group_id=self.id)
+ return True
+
+ def remove(self, photo):
+ """Remove a Photo from the group"""
+ method = 'flickr.groups.pools.remove'
+ _dopost(method, auth=True, photo_id=photo.id, group_id=self.id)
+ return True
+
+class Tag(object):
+ def __init__(self, id, author, raw, text):
+ self.id = id
+ self.author = author
+ self.raw = raw
+ self.text = text
+
+ def __str__(self):
+ return '<Flickr Tag %s (%s)>' % (self.id, self.text)
+
+
+#Flickr API methods
+#see api docs http://www.flickr.com/services/api/
+#for details of each param
+
+#XXX: Could be Photo.search(cls)
+def photos_search(user_id='', auth=False, tags='', tag_mode='', text='',\
+ min_upload_date='', max_upload_date='',\
+ min_taken_date='', max_taken_date='', \
+ license='', per_page='', page='', sort=''):
+ """Returns a list of Photo objects.
+
+ If auth=True then will auth the user. Can see private etc
+ """
+ method = 'flickr.photos.search'
+
+ data = _doget(method, auth=auth, user_id=user_id, tags=tags, text=text,\
+ min_upload_date=min_upload_date,\
+ max_upload_date=max_upload_date, \
+ min_taken_date=min_taken_date, \
+ max_taken_date=max_taken_date, \
+ license=license, per_page=per_page,\
+ page=page, sort=sort)
+ photos = []
+ if isinstance(data.rsp.photos.photo, list):
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ else:
+ photos = [_parse_photo(data.rsp.photos.photo)]
+ return photos
+
+#XXX: Could be class method in User
+def people_findByEmail(email):
+ """Returns User object."""
+ method = 'flickr.people.findByEmail'
+ data = _doget(method, find_email=email)
+ user = User(data.rsp.user.id, username=data.rsp.user.username.text)
+ return user
+
+def people_findByUsername(username):
+ """Returns User object."""
+ method = 'flickr.people.findByUsername'
+ data = _doget(method, username=username)
+ user = User(data.rsp.user.id, username=data.rsp.user.username.text)
+ return user
+
+#XXX: Should probably be in User as a list User.public
+def people_getPublicPhotos(user_id, per_page='', page=''):
+ """Returns list of Photo objects."""
+ method = 'flickr.people.getPublicPhotos'
+ data = _doget(method, user_id=user_id, per_page=per_page, page=page)
+ photos = []
+ if hasattr(data.rsp.photos, "photo"): # Check if there are photos at all (may be been paging too far).
+ if isinstance(data.rsp.photos.photo, list):
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ else:
+ photos = [_parse_photo(data.rsp.photos.photo)]
+ return photos
+
+#XXX: These are also called from User
+def favorites_getList(user_id='', per_page='', page=''):
+ """Returns list of Photo objects."""
+ method = 'flickr.favorites.getList'
+ data = _doget(method, auth=True, user_id=user_id, per_page=per_page,\
+ page=page)
+ photos = []
+ if isinstance(data.rsp.photos.photo, list):
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ else:
+ photos = [_parse_photo(data.rsp.photos.photo)]
+ return photos
+
+def favorites_getPublicList(user_id, per_page='', page=''):
+ """Returns list of Photo objects."""
+ method = 'flickr.favorites.getPublicList'
+ data = _doget(method, auth=False, user_id=user_id, per_page=per_page,\
+ page=page)
+ photos = []
+ if isinstance(data.rsp.photos.photo, list):
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ else:
+ photos = [_parse_photo(data.rsp.photos.photo)]
+ return photos
+
+def favorites_add(photo_id):
+ """Add a photo to the user's favorites."""
+ method = 'flickr.favorites.add'
+ _dopost(method, auth=True, photo_id=photo_id)
+ return True
+
+def favorites_remove(photo_id):
+ """Remove a photo from the user's favorites."""
+ method = 'flickr.favorites.remove'
+ _dopost(method, auth=True, photo_id=photo_id)
+ return True
+
+def groups_getPublicGroups():
+ """Get a list of groups the auth'd user is a member of."""
+ method = 'flickr.groups.getPublicGroups'
+ data = _doget(method, auth=True)
+ groups = []
+ if isinstance(data.rsp.groups.group, list):
+ for group in data.rsp.groups.group:
+ groups.append(Group(group.id, name=group.name))
+ else:
+ group = data.rsp.groups.group
+ groups = [Group(group.id, name=group.name)]
+ return groups
+
+def groups_pools_getGroups():
+ """Get a list of groups the auth'd user can post photos to."""
+ method = 'flickr.groups.pools.getGroups'
+ data = _doget(method, auth=True)
+ groups = []
+ if isinstance(data.rsp.groups.group, list):
+ for group in data.rsp.groups.group:
+ groups.append(Group(group.id, name=group.name, \
+ privacy=group.privacy))
+ else:
+ group = data.rsp.groups.group
+ groups = [Group(group.id, name=group.name, privacy=group.privacy)]
+ return groups
+
+
+def tags_getListUser(user_id=''):
+ """Returns a list of tags for the given user (in string format)"""
+ method = 'flickr.tags.getListUser'
+ auth = user_id == ''
+ data = _doget(method, auth=auth, user_id=user_id)
+ if isinstance(data.rsp.tags.tag, list):
+ return [tag.text for tag in data.rsp.tags.tag]
+ else:
+ return [data.rsp.tags.tag.text]
+
+def tags_getListUserPopular(user_id='', count=''):
+ """Gets the popular tags for a user in dictionary form tag=>count"""
+ method = 'flickr.tags.getListUserPopular'
+ auth = user_id == ''
+ data = _doget(method, auth=auth, user_id=user_id)
+ result = {}
+ if isinstance(data.rsp.tags.tag, list):
+ for tag in data.rsp.tags.tag:
+ result[tag.text] = tag.count
+ else:
+ result[data.rsp.tags.tag.text] = data.rsp.tags.tag.count
+ return result
+
+def tags_getrelated(tag):
+ """Gets the related tags for given tag."""
+ method = 'flickr.tags.getRelated'
+ data = _doget(method, auth=False, tag=tag)
+ if isinstance(data.rsp.tags.tag, list):
+ return [tag.text for tag in data.rsp.tags.tag]
+ else:
+ return [data.rsp.tags.tag.text]
+
+def contacts_getPublicList(user_id):
+ """Gets the contacts (Users) for the user_id"""
+ method = 'flickr.contacts.getPublicList'
+ data = _doget(method, auth=False, user_id=user_id)
+ if isinstance(data.rsp.contacts.contact, list):
+ return [User(user.nsid, username=user.username) \
+ for user in data.rsp.contacts.contact]
+ else:
+ user = data.rsp.contacts.contact
+ return [User(user.nsid, username=user.username)]
+
+def interestingness():
+ method = 'flickr.interestingness.getList'
+ data = _doget(method)
+ photos = []
+ if isinstance(data.rsp.photos.photo , list):
+ for photo in data.rsp.photos.photo:
+ photos.append(_parse_photo(photo))
+ else:
+ photos = [_parse_photo(data.rsp.photos.photo)]
+ return photos
+
+def test_login():
+ method = 'flickr.test.login'
+ data = _doget(method, auth=True)
+ user = User(data.rsp.user.id, username=data.rsp.user.username.text)
+ return user
+
+def test_echo():
+ method = 'flickr.test.echo'
+ data = _doget(method)
+ return data.rsp.stat
+
+
+#useful methods
+
+def _doget(method, auth=False, **params):
+ #uncomment to check you aren't killing the flickr server
+ #print "***** do get %s" % method
+
+ #convert lists to strings with ',' between items
+ for (key, value) in params.items():
+ if isinstance(value, list):
+ params[key] = ','.join([item for item in value])
+
+ url = '%s%s/?api_key=%s&method=%s&%s'% \
+ (HOST, API, API_KEY, method, urlencode(params))
+ if auth:
+ url = url + '&email=%s&password=%s' % (email, password)
+
+ #another useful debug print statement
+ #print url
+
+ xml = minidom.parse(urlopen(url))
+ data = unmarshal(xml)
+ if not data.rsp.stat == 'ok':
+ msg = "ERROR [%s]: %s" % (data.rsp.err.code, data.rsp.err.msg)
+ raise FlickrError, msg
+ return data
+
+def _dopost(method, auth=False, **params):
+ #uncomment to check you aren't killing the flickr server
+ #print "***** do post %s" % method
+
+ #convert lists to strings with ',' between items
+ for (key, value) in params.items():
+ if isinstance(value, list):
+ params[key] = ','.join([item for item in value])
+
+ url = '%s%s/' % (HOST, API)
+
+ payload = 'api_key=%s&method=%s&%s'% \
+ (API_KEY, method, urlencode(params))
+ if auth:
+ payload = payload + '&email=%s&password=%s' % (email, password)
+
+ #another useful debug print statement
+ #print url
+ #print payload
+
+ xml = minidom.parse(urlopen(url, payload))
+ data = unmarshal(xml)
+ if not data.rsp.stat == 'ok':
+ msg = "ERROR [%s]: %s" % (data.rsp.err.code, data.rsp.err.msg)
+ raise FlickrError, msg
+ return data
+
+def _parse_photo(photo):
+ """Create a Photo object from photo data."""
+ owner = User(photo.owner)
+ title = photo.title
+ ispublic = photo.ispublic
+ isfriend = photo.isfriend
+ isfamily = photo.isfamily
+ secret = photo.secret
+ server = photo.server
+ p = Photo(photo.id, owner=owner, title=title, ispublic=ispublic,\
+ isfriend=isfriend, isfamily=isfamily, secret=secret, \
+ server=server)
+ return p
+
+#stolen methods
+
+class Bag: pass
+
+#unmarshal taken and modified from pyamazon.py
+#makes the xml easy to work with
+def unmarshal(element):
+ rc = Bag()
+ if isinstance(element, minidom.Element):
+ for key in element.attributes.keys():
+ setattr(rc, key, element.attributes[key].value)
+
+ childElements = [e for e in element.childNodes \
+ if isinstance(e, minidom.Element)]
+ if childElements:
+ for child in childElements:
+ key = child.tagName
+ if hasattr(rc, key):
+ if type(getattr(rc, key)) <> type([]):
+ setattr(rc, key, [getattr(rc, key)])
+ setattr(rc, key, getattr(rc, key) + [unmarshal(child)])
+ elif isinstance(child, minidom.Element) and \
+ (child.tagName == 'Details'):
+ # make the first Details element a key
+ setattr(rc,key,[unmarshal(child)])
+ #dbg: because otherwise 'hasattr' only tests
+ #dbg: on the second occurence: if there's a
+ #dbg: single return to a query, it's not a
+ #dbg: list. This module should always
+ #dbg: return a list of Details objects.
+ else:
+ setattr(rc, key, unmarshal(child))
+ else:
+ #jec: we'll have the main part of the element stored in .text
+ #jec: will break if tag <text> is also present
+ text = "".join([e.data for e in element.childNodes \
+ if isinstance(e, minidom.Text)])
+ setattr(rc, 'text', text)
+ return rc
+
+#unique items from a list from the cookbook
+def uniq(alist): # Fastest without order preserving
+ set = {}
+ map(set.__setitem__, alist, [])
+ return set.keys()
+
+if __name__ == '__main__':
+ print test_echo()
diff --git a/app/lib/upload/models.py b/app/lib/upload/models.py
new file mode 100644
index 0000000..ae2dab4
--- /dev/null
+++ b/app/lib/upload/models.py
@@ -0,0 +1,48 @@
+import datetime
+from django.db import models
+from django.conf import settings
+from django.template.defaultfilters import slugify
+import mimetypes
+
+class FileUpload(models.Model):
+ upload_date = models.DateTimeField(auto_now_add=True)
+ upload = models.FileField(upload_to="images/%s" %(datetime.datetime.today().strftime("%Y")))
+ title = models.CharField(max_length=100)
+ description = models.CharField(blank=True, max_length=200)
+ content_type = models.CharField(editable=False, max_length=100)
+ sub_type = models.CharField(editable=False, max_length=100)
+
+
+
+ class Meta:
+ ordering = ['upload_date', 'title']
+
+ def __unicode__(self):
+ return self.title
+
+ def mime_type(self):
+ return '%s/%s' % (self.content_type, self.sub_type)
+
+ def type_slug(self):
+ return slugify(self.sub_type)
+
+ def is_image(self):
+ if self.content_type == 'image':
+ return True
+ else:
+ return False
+
+ def get_absolute_url(self):
+ return '%s%s' % (settings.IMAGES_URL, self.upload.url[33:])
+
+ def save(self, *args, **kwargs):
+ file_path = '%s%s' % (settings.MEDIA_ROOT, self.upload)
+ (mime_type, encoding) = mimetypes.guess_type(file_path)
+ try:
+ [self.content_type, self.sub_type] = mime_type.split('/')
+ except:
+ self.content_type = 'text'
+ self.sub_type = 'plain'
+ super(FileUpload, self).save(*args, **kwargs)
+
+ \ No newline at end of file
diff --git a/app/lib/upload/urls.py b/app/lib/upload/urls.py
new file mode 100644
index 0000000..c95d71f
--- /dev/null
+++ b/app/lib/upload/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import *
+import views as upload_views
+urlpatterns = patterns('',
+ url(r'download/$', upload_views.download),
+ url(r'youtube/$', upload_views.youtube),
+ url(r'flickr/$', upload_views.flickr),
+ url(r'images/$', upload_views.images),
+ url(r'files/$', upload_views.files),
+ url(r'^$', upload_views.all),
+)
diff --git a/app/lib/upload/views.py b/app/lib/upload/views.py
new file mode 100644
index 0000000..53ac642
--- /dev/null
+++ b/app/lib/upload/views.py
@@ -0,0 +1,95 @@
+from models import FileUpload
+from django.shortcuts import render_to_response
+from django.http import HttpResponse, Http404
+from django.template import RequestContext
+from django.conf import settings
+import urllib, urlparse, datetime
+
+def all(request):
+ if not request.user.is_staff:
+ raise Http404
+ files = FileUpload.objects.all().order_by('-upload_date')
+ return render_to_response('upload/base.html', {'files': files, 'textarea_id': request.GET['textarea']}, context_instance=RequestContext(request))
+
+def images(request):
+ if not request.user.is_staff:
+ raise Http404
+ files = FileUpload.objects.filter(content_type = 'image').order_by('-upload_date')
+ return render_to_response('upload/base.html', {'files': files, 'textarea_id': request.GET['textarea']}, context_instance=RequestContext(request))
+
+def files(request):
+ if not request.user.is_staff:
+ raise Http404
+ not_files = ['video', 'image']
+ files = FileUpload.objects.exclude(content_type__in = not_files).order_by('-upload_date')
+ return render_to_response('upload/base.html', {'files': files, 'textarea_id': request.GET['textarea']}, context_instance=RequestContext(request))
+
+def youtube(request):
+ if not request.user.is_staff:
+ raise Http404
+ import elementtree.ElementTree as ET
+ try:
+ user = settings.YOU_TUBE_USER
+ needs_user_setting = False
+ except AttributeError:
+ user = 'NBC'
+ needs_user_setting = True
+ gdata_feed = "http://gdata.youtube.com/feeds/videos?author=%s&orderby=updated" % (user,)
+ root = ET.parse(urllib.urlopen(gdata_feed)).getroot()
+ videos = []
+ for e in root.findall('{http://www.w3.org/2005/Atom}entry'):
+ video = {}
+ video['title'] = e.findtext('{http://www.w3.org/2005/Atom}title')
+ date = e.findtext('{http://www.w3.org/2005/Atom}published').split('T')[0]
+ video['upload_date'] = date
+ media = e.find('{http://search.yahoo.com/mrss/}group')
+ video['description'] = media.findtext('{http://search.yahoo.com/mrss/}description')
+ video['thumb'] = media.find('{http://search.yahoo.com/mrss/}thumbnail').attrib['url']
+ video['image'] = media.findall('{http://search.yahoo.com/mrss/}thumbnail')[-1].attrib['url']
+ video['url'] = media.find('{http://search.yahoo.com/mrss/}content').attrib['url']
+ videos.append(video)
+ return render_to_response('upload/youtube.html', {'videos': videos, 'textarea_id': request.GET['textarea'], 'needs_user_setting': needs_user_setting}, context_instance=RequestContext(request))
+
+def flickr(request):
+ if not request.user.is_staff:
+ raise Http404
+ import flickr
+ try:
+ user = settings.FLICKR_USER
+ flickr.API_KEY = settings.FLICKR_API_KEY
+ except AttributeError:
+ return HttpResponse('You need to set <tt>FLICKR_USER</tt> and <tt>FLICKR_API_KEY</tt> in your settings file. <br />&larr; <a href="/uploads/?textarea=%s">Back to all uploads.</a>' % (request.GET['textarea'],))
+ # Get first 12 photos for the user
+ flickr_photos = flickr.people_getPublicPhotos(user, 12, 1)
+ photos = []
+ #this loop is too slow. needs caching or a better library?
+ for f in flickr_photos:
+ photo = {}
+ photo['url'] = f.getURL('Small', 'source')
+ photo['link'] = f.getURL()
+ photo['title'] = f._Photo__title
+ photo['upload_date'] = datetime.datetime.fromtimestamp(float(f._Photo__dateposted))
+ photos.append(photo)
+ return render_to_response('upload/flickr.html', {'photos': photos, 'textarea_id': request.GET['textarea']}, context_instance=RequestContext(request))
+
+def download(request):
+ '''Saves image from URL and returns ID for use with AJAX script'''
+ if not request.user.is_staff:
+ raise Http404
+ if request.method == 'GET':
+ #f = FileUpload();
+ #f.title = request.GET['title'] or 'untitled'
+ #f.description = request.GET['description']
+ url = urllib.unquote(request.GET['photo'])
+ file_content = urllib.urlopen(url).read()
+ file_name = url.split('/')[-1]
+ fullpath = '%s%s/%s' %(settings.IMAGES_ROOT,datetime.datetime.today().strftime("%Y"),file_name)
+ photo = urllib.urlretrieve(request.GET['photo'], fullpath)
+ f, created = FileUpload.objects.get_or_create(
+ title = request.GET['title'] or 'untitled',
+ description = request.GET['description'],
+ upload = 'images/%s/%s' %(datetime.datetime.today().strftime("%Y"),file_name),
+ )
+ return HttpResponse('%s/?_popup=1' % (f.id))
+ else:
+ raise Http404 \ No newline at end of file
diff --git a/app/lttr/__init__.py b/app/lttr/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lttr/__init__.py
diff --git a/app/lttr/admin.py b/app/lttr/admin.py
new file mode 100644
index 0000000..58f9d84
--- /dev/null
+++ b/app/lttr/admin.py
@@ -0,0 +1,62 @@
+from django.contrib import admin
+
+from utils.widgets import AdminImageWidget, LGEntryForm
+
+from .models import (
+ NewsletterMailing,
+ Subscriber,
+ Newsletter,
+ MailingStatus,
+)
+
+@admin.register(Subscriber)
+class SubscriberAdmin(admin.ModelAdmin):
+ list_display = ('email_field', 'user', 'newsletter', 'date_created', 'subscribed', 'subscribe_date', 'unsubscribed')
+ search_fields = ['email_field']
+ list_filter = ['unsubscribed', 'newsletter']
+
+ class Media:
+ js = ('next-prev-links.js',)
+
+
+@admin.register(Newsletter)
+class NewsletterAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(MailingStatus)
+class MailingStatusAdmin(admin.ModelAdmin):
+ list_display = ('newsletter_mailing', 'subscriber', 'status', 'creation_date', 'newsletter')
+ list_filter = ('status', 'creation_date', 'newsletter_mailing__newsletter')
+
+
+@admin.register(NewsletterMailing)
+class NewsletterMailingAdmin(admin.ModelAdmin):
+ form = LGEntryForm
+ list_display = ('title', 'pub_date', 'newsletter', 'post')
+ list_filter = ['newsletter']
+ fieldsets = (
+ ('Entry', {
+ 'fields': (
+ ("newsletter", "post"),
+ 'title',
+ 'subtitle',
+ 'body_markdown',
+ 'body_html',
+ 'body_email_html',
+ 'pub_date',
+ 'featured_image',
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }
+ ),
+ )
+ class Media:
+ js = ('image-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
diff --git a/app/lttr/forms.py b/app/lttr/forms.py
new file mode 100644
index 0000000..e3fb272
--- /dev/null
+++ b/app/lttr/forms.py
@@ -0,0 +1,100 @@
+from django import forms
+from django.forms.utils import ValidationError
+
+from .validators import validate_email_nouser
+from .models import Subscriber
+
+
+class SubscribeForm(forms.ModelForm):
+
+ class Meta:
+ model = Subscriber
+ fields = ['user']
+
+
+class NewsletterForm(forms.ModelForm):
+ """ This is the base class for all forms managing subscriptions. """
+ email_field = forms.EmailField(label='Email Address:', widget=forms.EmailInput(attrs={'placeholder': 'Your email address'}))
+
+ class Meta:
+ model = Subscriber
+ fields = ('email_field',)
+
+ def __init__(self, *args, **kwargs):
+
+ assert 'newsletter' in kwargs, 'No newsletter specified'
+
+ newsletter = kwargs.pop('newsletter')
+
+ if 'ip' in kwargs:
+ ip = kwargs['ip']
+ del kwargs['ip']
+ else:
+ ip = None
+
+ super(NewsletterForm, self).__init__(*args, **kwargs)
+
+ self.instance.newsletter = newsletter
+
+ if ip:
+ self.instance.ip = ip
+
+
+class SubscribeRequestForm(NewsletterForm):
+ """
+ Request subscription to the newsletter. Will result in an activation email
+ being sent with a link where one can edit, confirm and activate one's
+ subscription.
+ """
+ email_field = forms.EmailField(
+ label=("e-mail"), validators=[validate_email_nouser], widget=forms.EmailInput(attrs={'placeholder': 'Your email address'})
+ )
+
+ def clean_email_field(self):
+ data = self.cleaned_data['email_field']
+
+ # Check whether we have already been subscribed to
+ try:
+ subscription = Subscriber.objects.get(
+ email_field__exact=data,
+ newsletter=self.instance.newsletter
+ )
+
+ if subscription.subscribed and not subscription.unsubscribed:
+ raise ValidationError(
+ "I appreciate the effort, but you're already subscribed. (if you're not receiving newsletters, email me at sng@luxagraf.net and I will see what's going on)"
+ )
+ else:
+ self.instance = subscription
+
+ self.instance = subscription
+
+ except Subscriber.DoesNotExist:
+ pass
+
+ return data
+
+
+class UpdateForm(NewsletterForm):
+ """
+ This form allows one to actually update to or unsubscribe from the
+ newsletter. To do this, a correct activation code is required.
+ """
+
+ email_field = forms.EmailField(
+ label=("e-mail"), validators=[validate_email_nouser], disabled=True
+ )
+
+ def clean_user_activation_code(self):
+ data = self.cleaned_data['user_activation_code']
+
+ if data != self.instance.activation_code:
+ raise ValidationError(
+ ('The validation code supplied by you does not match.')
+ )
+
+ return data
+
+ user_activation_code = forms.CharField(
+ label=("Activation code"), max_length=40
+ )
diff --git a/app/lttr/mailer.py b/app/lttr/mailer.py
new file mode 100644
index 0000000..40f1003
--- /dev/null
+++ b/app/lttr/mailer.py
@@ -0,0 +1,83 @@
+from time import sleep
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+from django.core.mail import EmailMultiAlternatives
+from django.template.loader import render_to_string
+from django.utils.encoding import smart_str
+
+from .models import Subscriber, MailingStatus
+
+
+
+class SendShit():
+
+ def __init__(self, newsletter, mailing, verbose=0):
+ self.verbose = verbose
+ self.newsletter = newsletter
+ self.mailing = mailing
+ all_subscribers = Subscriber.objects.filter(newsletter=self.newsletter,subscribed=True,unsubscribed=False).values('email_field')
+ already_sent = MailingStatus.objects.filter(newsletter_mailing=self.mailing,status=1).values('subscriber__email_field')
+ self.subscribers = all_subscribers.difference(already_sent)
+
+
+ def send_mailings(self):
+ mailings = len(self.subscribers)
+ print("mailing newsletter to %s subscribers"%mailings)
+ i = 1
+ for s in self.subscribers:
+ subscriber = Subscriber.objects.get(newsletter=self.newsletter,email_field=s['email_field'],subscribed=True,unsubscribed=False)
+ status = None
+ if self.verbose == 1:
+ print("mailing newsletter %s of %s to %s" %(i,mailings,subscriber))
+ status, created = MailingStatus.objects.get_or_create(
+ newsletter_mailing=self.mailing,
+ subscriber=subscriber,
+ )
+ # New instance, try sending
+ if created:
+ email = self.build_message(subscriber)
+ status.status=1
+ if self.verbose==1:
+ print("successfully sent %s the newsletter mailing %s"%(subscriber, self.mailing))
+ status.save()
+ else:
+ # not new, check if error and resend or just continue
+ if status.status == 2:
+ if self.verbose==1:
+ print("retrying error")
+ try:
+ email = self.build_message(subscriber)
+ status.status=1
+ if self.verbose==1:
+ print("successfully sent %s the newsletter mailing %s"%(subscriber, self.mailing))
+ except:
+ status.status=2
+ if self.verbose == 1:
+ print("failed to send %s to %s"%(self.mailing, subscriber))
+ status.save()
+ i=i+1
+ sleep(2)
+
+
+ def build_message(self, subscriber):
+ """
+ Build the email as plain text with a
+ a multipart alternative for HTML
+ """
+ subject = smart_str("%s: %s — %s" %(self.mailing.newsletter.title, self.mailing.get_issue_str(), self.mailing.title))
+ from_email, to = 'Scott Gilbertson <sng@luxagraf.net>', subscriber.get_email()
+ text_content = render_to_string(self.newsletter.get_template_plain(), {'object': self.mailing, 'subscriber':subscriber})
+ html_content = render_to_string(self.newsletter.get_template_html(), {'object': self.mailing, 'subscriber':subscriber})
+ #print(html_content)
+ msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
+ msg.attach_alternative(html_content, "text/html")
+ #msg.Header.Add('List-Unsubscribe', '<https://luxagraf.net%s>' % subscriber.unsubscribe_activate_url)
+ msg.send()
+
+ '''
+ for header, value in self.newsletter.server.custom_headers.items():
+ message[header] = value
+ '''
+ return msg
+
diff --git a/app/lttr/management/commands/send_newsletter.py b/app/lttr/management/commands/send_newsletter.py
new file mode 100644
index 0000000..0f36183
--- /dev/null
+++ b/app/lttr/management/commands/send_newsletter.py
@@ -0,0 +1,31 @@
+"""Command for sending the newsletter"""
+from django.conf import settings
+from django.utils.translation import activate
+from django.core.management.base import NoArgsCommand
+
+from lttr.mailer import Mailer
+from lttr.models import NewsletterMailing
+
+
+class Command(NoArgsCommand):
+ """Send the newsletter in queue"""
+ help = 'Send the newsletter in queue'
+
+ def handle_noargs(self, **options):
+ verbose = int(options['verbosity'])
+
+ if verbose:
+ print('Starting sending newsletters...')
+
+ activate(settings.LANGUAGE_CODE)
+
+ for newsletter in NewsletterMailing.objects.exclude(
+ status=Newsletter.DRAFT).exclude(status=Newsletter.SENT):
+ mailer = Mailer(newsletter, verbose=verbose)
+ if mailer.can_send:
+ if verbose:
+ print('Start emailing %s' % newsletter.title)
+ mailer.run()
+
+ if verbose:
+ print('End session sending')
diff --git a/app/lttr/migrations/0001_initial.py b/app/lttr/migrations/0001_initial.py
new file mode 100644
index 0000000..557df91
--- /dev/null
+++ b/app/lttr/migrations/0001_initial.py
@@ -0,0 +1,78 @@
+# Generated by Django 3.2.8 on 2022-02-04 20:50
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import lttr.models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('posts', '0002_alter_post_post_type'),
+ ('media', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Newsletter',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=250)),
+ ('slug', models.SlugField(unique=True)),
+ ('intro', models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Subscriber',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email_field', models.EmailField(blank=True, db_column='email', db_index=True, max_length=254, null=True)),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_updated', models.DateTimeField(auto_now=True)),
+ ('activation_code', models.CharField(default=lttr.models.make_activation_code, max_length=40)),
+ ('subscribed', models.BooleanField(db_index=True, default=False)),
+ ('subscribe_date', models.DateTimeField(blank=True, null=True)),
+ ('unsubscribed', models.BooleanField(db_index=True, default=False)),
+ ('unsubscribe_date', models.DateTimeField(blank=True, null=True)),
+ ('newsletter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.newsletter')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='NewsletterMailing',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(blank=True, max_length=250)),
+ ('subtitle', models.CharField(blank=True, max_length=250, null=True)),
+ ('body_html', models.TextField(blank=True)),
+ ('body_email_html', models.TextField(blank=True)),
+ ('body_markdown', models.TextField(blank=True)),
+ ('pub_date', models.DateTimeField(blank=True)),
+ ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')),
+ ('newsletter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.newsletter')),
+ ('post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='posts.post')),
+ ],
+ options={
+ 'ordering': ('-title', '-pub_date'),
+ },
+ ),
+ migrations.CreateModel(
+ name='MailingStatus',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('status', models.IntegerField(choices=[(0, 'Initialized'), (1, 'Sent'), (2, 'Error')], null=True)),
+ ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
+ ('newsletter_mailing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.newslettermailing', verbose_name='newsletter')),
+ ('subscriber', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lttr.subscriber', verbose_name='subscriber')),
+ ],
+ options={
+ 'verbose_name': 'subscriber mailing status',
+ 'verbose_name_plural': 'subscriber mailing statuses',
+ 'ordering': ('-creation_date',),
+ },
+ ),
+ ]
diff --git a/app/lttr/migrations/__init__.py b/app/lttr/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lttr/migrations/__init__.py
diff --git a/app/lttr/models.py b/app/lttr/models.py
new file mode 100644
index 0000000..5fed036
--- /dev/null
+++ b/app/lttr/models.py
@@ -0,0 +1,401 @@
+import datetime
+
+from django.dispatch import receiver
+from django.contrib.gis.db import models
+from django.db.models.signals import post_save
+from django.contrib.sites.models import Site
+from django.template.loader import select_template
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+from django.utils.text import slugify
+from django.urls import reverse
+from django.core.mail import send_mail
+from django.conf import settings
+from django.utils.crypto import get_random_string
+from django.core.mail import EmailMultiAlternatives
+
+from django.template import Context, Template
+
+from bs4 import BeautifulSoup
+
+from taggit.managers import TaggableManager
+
+from utils.util import render_images, parse_video, markdown_to_html
+from taxonomy.models import TaggedItems
+from media.models import LuxImage, LuxImageSize
+from posts.models import Post
+
+
+# Possible actions that user can perform
+ACTIONS = ('subscribe', 'unsubscribe', 'update')
+
+
+def markdown_to_emailhtml(base_html):
+ soup = BeautifulSoup(base_html, "lxml")
+ for p in soup.find_all('p'):
+ p.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;font-size:17px;line-height:1.5;hyphens:auto;color:#222222;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;"
+ for i in soup.find_all('img'):
+ i.attrs['width']="720"
+ i.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:inline;margin-bottom:0;width:100% !important;max-width:100% !important;height:auto !important;max-height:auto !important;"
+ for h in soup.find_all('hr'):
+ h.attrs['style']="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:20%;margin-top:40px;margin-bottom:40px;margin-right:0px;margin-left:0px;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;"
+ return str(soup)[12:-14]
+
+
+def make_activation_code():
+ """ Generate a unique activation code. """
+
+ # Use Django's crypto get_random_string() instead of rolling our own.
+ return get_random_string(length=40)
+
+
+class Newsletter(models.Model):
+ """ A model for Newletters. Might I one day have two? I might. """
+ title = models.CharField(max_length=250)
+ slug = models.SlugField(db_index=True, unique=True)
+ intro = models.TextField(blank=True, null=True)
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("lttr:detail", kwargs={"slug": self.slug})
+
+ def subscribe_url(self):
+ return reverse('lttr:newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug})
+
+ def unsubscribe_url(self):
+ return reverse('newsletter_unsubscribe_request', kwargs={'newsletter_slug': self.slug})
+
+ def update_url(self):
+ return reverse('newsletter_update_request', kwargs={'newsletter_slug': self.slug})
+
+ def archive_url(self):
+ return reverse('newsletter_archive', kwargs={'newsletter_slug': self.slug})
+
+ def get_subscriptions(self):
+ return Subscriber.objects.filter(newsletter=self, subscribed=True)
+
+ def get_template_plain(self):
+ return 'lttr/emails/%s_plain_text_email.txt' % self.slug
+
+ def get_template_html(self):
+ return 'lttr/emails/%s_html_email.html' % self.slug
+
+ def get_templates(self, action):
+ """
+ Return a subject, text, HTML tuple with e-mail templates for
+ a particular action. Returns a tuple with subject, text and e-mail
+ template.
+ """
+
+ assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action
+
+ # Common substitutions for filenames
+ tpl_subst = {
+ 'action': action,
+ 'newsletter': self.slug
+ }
+
+ # Common root path for all the templates
+ tpl_root = 'lttr/message/'
+
+ subject_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst,
+ tpl_root + '%(action)s_subject.txt' % tpl_subst,
+ ])
+
+ text_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst,
+ tpl_root + '%(action)s.txt' % tpl_subst,
+ ])
+
+ html_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst,
+ tpl_root + '%(action)s.html' % tpl_subst,
+ ])
+
+ return (subject_template, text_template, html_template)
+
+ def get_sender(self):
+ return 'Scott Gilbertson <sng@luxagraf.net>'
+
+ @classmethod
+ def get_default(cls):
+ try:
+ return cls.objects.all()[0].pk
+ except IndexError:
+ return None
+
+
+
+class NewsletterMailing(models.Model):
+ """ A model for Newletter Mailings, the things actually sent out """
+ newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
+ post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.SET_NULL)
+ title = models.CharField(max_length=250, blank=True)
+ subtitle = models.CharField(max_length=250, null=True, blank=True)
+ body_html = models.TextField(blank=True)
+ body_email_html = models.TextField(blank=True)
+ body_markdown = models.TextField(blank=True)
+ pub_date = models.DateTimeField(blank=True)
+ featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True)
+
+ class Meta:
+ ordering = ('-title', '-pub_date')
+
+ def __str__(self):
+ return self.title
+
+ def email_encode(self):
+ return self.body_markdown
+
+ def save(self, *args, **kwargs):
+ created = self.pk is None
+ if created and self.post:
+ self.title = self.post.title
+ self.subtitle = self.post.subtitle
+ self.body_markdown = self.post.body_markdown
+ self.pub_date = self.post.pub_date
+ self.featured_image = self.post.featured_image
+ self.issue = self.post.issue
+ if not created:
+ md = render_images(self.body_markdown)
+ self.body_html = markdown_to_html(md)
+ self.body_email_html = markdown_to_emailhtml(self.body_html)
+ self.date_created = timezone.now()
+ self.issue = self.post.issue
+ if created and not self.featured_image:
+ self.featured_image = LuxImage.objects.latest()
+ super(NewsletterMailing, self).save()
+
+
+class Subscriber(models.Model):
+ """ A model for Newletter Subscriber """
+ email_field = models.EmailField(db_column='email', db_index=True, blank=True, null=True)
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
+ date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False)
+ date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False)
+ newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
+ activation_code = models.CharField(max_length=40, default=make_activation_code)
+ subscribed = models.BooleanField(default=False, db_index=True)
+ subscribe_date = models.DateTimeField(null=True, blank=True)
+ unsubscribed = models.BooleanField(default=False, db_index=True)
+ unsubscribe_date = models.DateTimeField(null=True, blank=True)
+
+ def __str__(self):
+ if self.user:
+ return self.user.username
+ return self.email_field
+
+ def get_name(self):
+ if self.user:
+ return self.user.get_full_name()
+
+ def get_email(self):
+ if self.user:
+ return self.user.email
+ return self.email_field
+
+ def set_email(self, email):
+ if not self.user:
+ self.email_field = email
+ email = property(get_email, set_email)
+
+ def update(self, action):
+ """
+ Update subscription according to requested action:
+ subscribe/unsubscribe/update/, then save the changes.
+ """
+
+ assert action in ('subscribe', 'update', 'unsubscribe')
+
+ # If a new subscription or update, make sure it is subscribed
+ # Else, unsubscribe
+ if action == 'subscribe' or action == 'update':
+ self.subscribed = True
+ else:
+ self.unsubscribed = True
+
+ # This triggers the subscribe() and/or unsubscribe() methods, taking
+ # care of stuff like maintaining the (un)subscribe date.
+ self.save()
+
+ def _subscribe(self):
+ """
+ Internal helper method for managing subscription state
+ during subscription.
+ """
+
+ self.subscribe_date = datetime.datetime.now()
+ self.subscribed = True
+ self.unsubscribed = False
+
+ def _unsubscribe(self):
+ """
+ Internal helper method for managing subscription state
+ during unsubscription.
+ """
+ self.subscribed = False
+ self.unsubscribed = True
+ self.unsubscribe_date = datetime.datetime.now()
+
+ def save(self, *args, **kwargs):
+ """
+ Perform some basic validation and state maintenance of Subscription.
+ TODO: Move this code to a more suitable place (i.e. `clean()`) and
+ cleanup the code. Refer to comment below and
+ https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
+ """
+
+ # This is a lame way to find out if we have changed but using Django
+ # API internals is bad practice. This is necessary to discriminate
+ # from a state where we have never been subscribed but is mostly for
+ # backward compatibility. It might be very useful to make this just
+ # one attribute 'subscribe' later. In this case unsubscribed can be
+ # replaced by a method property.
+
+ if self.pk:
+ assert(Subscriber.objects.filter(pk=self.pk).count() == 1)
+
+ subscription = Subscriber.objects.get(pk=self.pk)
+ old_subscribed = subscription.subscribed
+ old_unsubscribed = subscription.unsubscribed
+
+ # If we are subscribed now and we used not to be so, subscribe.
+ # If we user to be unsubscribed but are not so anymore, subscribe.
+ if ((self.subscribed and not old_subscribed) or
+ (old_unsubscribed and not self.unsubscribed)):
+ self._subscribe()
+
+ assert not self.unsubscribed
+ assert self.subscribed
+
+ # If we are unsubcribed now and we used not to be so, unsubscribe.
+ # If we used to be subscribed but are not subscribed anymore,
+ # unsubscribe.
+ elif ((self.unsubscribed and not old_unsubscribed) or
+ (old_subscribed and not self.subscribed)):
+ self._unsubscribe()
+
+ assert not self.subscribed
+ assert self.unsubscribed
+ else:
+ if self.subscribed:
+ self._subscribe()
+ elif self.unsubscribed:
+ self._unsubscribe()
+
+ super(Subscriber, self).save(*args, **kwargs)
+
+ def get_recipient(self):
+ return get_address(self.name, self.email)
+
+ def send_activation_email(self, action):
+ assert action in ACTIONS, 'Unknown action: %s' % action
+
+ (subject_template, text_template, html_template) = \
+ self.newsletter.get_templates(action)
+
+ variable_dict = {
+ 'subscription': self,
+ 'site': Site.objects.get_current(),
+ 'newsletter': self.newsletter,
+ 'date': self.subscribe_date,
+ 'STATIC_URL': settings.STATIC_URL,
+ 'MEDIA_URL': settings.MEDIA_URL
+ }
+
+ subject = subject_template.render(variable_dict).strip()
+ text = text_template.render(variable_dict)
+
+ message = EmailMultiAlternatives(
+ subject, text,
+ from_email=self.newsletter.get_sender(),
+ to=[self.email]
+ )
+
+ if html_template:
+ message.attach_alternative(
+ html_template.render(variable_dict), "text/html"
+ )
+
+ message.send()
+
+ def subscribe_activate_url(self):
+ return reverse('lttr:newsletter_activate', kwargs={
+ 'slug': self.newsletter.slug,
+ 'activation_code': self.activation_code
+ })
+
+ def unsubscribe_activate_url(self):
+ return reverse('lttr:newsletter_unsubscribe', kwargs={
+ 'slug': self.newsletter.slug,
+ 'activation_code': self.activation_code
+ })
+
+ def update_activate_url(self):
+ return reverse('lttr:newsletter_update_activate', kwargs={
+ 'slug': self.newsletter.slug,
+ 'action': 'update',
+ 'activation_code': self.activation_code
+ })
+
+
+def get_address(name, email):
+ if name:
+ return u'%s <%s>' % (name, email)
+ else:
+ return u'%s' % email
+
+
+class StatusType(models.IntegerChoices):
+ INIT = 0, ('Initialized')
+ SENT = 1, ('Sent')
+ ERROR = 2, ('Error')
+
+
+class MailingStatus(models.Model):
+ newsletter_mailing = models.ForeignKey(NewsletterMailing, on_delete=models.CASCADE, verbose_name=_('newsletter'))
+ subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE, verbose_name=_('subscriber'))
+ status = models.IntegerField(choices=StatusType.choices, null=True)
+ creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
+
+ def newsletter(self):
+ return self.newsletter_mailing.newsletter
+
+ def __str__(self):
+ return '%s : %s : %s' % (self.newsletter_mailing,
+ self.subscriber,
+ self.get_status_display())
+
+ class Meta:
+ ordering = ('-creation_date',)
+ verbose_name = _('subscriber mailing status')
+ verbose_name_plural = _('subscriber mailing statuses')
+
+
+'''
+from lttr.mailer import SendShit
+mailing = NewsletterMailing.objects.get(pk=1)
+newsletter = Newsletter.objects.get(pk=3)
+n = SendShit(newsletter, mailing, 1)
+n.send_mailings()
+'''
+
+
+def send_notification_email(newsletter, message, instance):
+ recipient_list = ['sng@luxagraf.net',]
+ subject = _('[%(site)s] New Subscriber to "%(object)s"') % {
+ 'site': Site.objects.get_current().name,
+ 'object': newsletter,
+ }
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
+
+
+@receiver(post_save, sender=Subscriber)
+def post_save_events(sender, update_fields, created, instance, **kwargs):
+ if instance.subscribed:
+ message = "%s has signed up for %s." %(instance.email_field, instance.newsletter)
+ send_notification_email(instance.newsletter, message, instance)
+
diff --git a/app/lttr/modelsnl.py b/app/lttr/modelsnl.py
new file mode 100644
index 0000000..9d2a7cd
--- /dev/null
+++ b/app/lttr/modelsnl.py
@@ -0,0 +1,719 @@
+import logging
+import time
+import django
+
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.contrib.sites.managers import CurrentSiteManager
+from django.core.mail import EmailMultiAlternatives
+from django.db import models
+from django.template.loader import select_template
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.functional import cached_property
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext
+from django.utils.timezone import now
+
+from sorl.thumbnail import ImageField
+from distutils.version import LooseVersion
+
+
+from .compat import get_context, reverse
+from .utils import (
+ make_activation_code, get_default_sites, ACTIONS
+)
+
+logger = logging.getLogger(__name__)
+
+AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+
+
+@python_2_unicode_compatible
+class Newsletter(models.Model):
+ site = models.ManyToManyField(Site, default=get_default_sites)
+
+ title = models.CharField(
+ max_length=200, verbose_name=_('newsletter title')
+ )
+ slug = models.SlugField(db_index=True, unique=True)
+
+ email = models.EmailField(
+ verbose_name=_('e-mail'), help_text=_('Sender e-mail')
+ )
+ sender = models.CharField(
+ max_length=200, verbose_name=_('sender'), help_text=_('Sender name')
+ )
+
+ visible = models.BooleanField(
+ default=True, verbose_name=_('visible'), db_index=True
+ )
+
+ send_html = models.BooleanField(
+ default=True, verbose_name=_('send html'),
+ help_text=_('Whether or not to send HTML versions of e-mails.')
+ )
+
+ objects = models.Manager()
+
+ # Automatically filter the current site
+ on_site = CurrentSiteManager()
+
+ def get_templates(self, action):
+ """
+ Return a subject, text, HTML tuple with e-mail templates for
+ a particular action. Returns a tuple with subject, text and e-mail
+ template.
+ """
+
+ assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action
+
+ # Common substitutions for filenames
+ tpl_subst = {
+ 'action': action,
+ 'newsletter': self.slug
+ }
+
+ # Common root path for all the templates
+ tpl_root = 'newsletter/message/'
+
+ subject_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst,
+ tpl_root + '%(action)s_subject.txt' % tpl_subst,
+ ])
+
+ text_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst,
+ tpl_root + '%(action)s.txt' % tpl_subst,
+ ])
+
+ if self.send_html:
+ html_template = select_template([
+ tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst,
+ tpl_root + '%(action)s.html' % tpl_subst,
+ ])
+ else:
+ # HTML templates are not required
+ html_template = None
+
+ return (subject_template, text_template, html_template)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ verbose_name = _('newsletter')
+ verbose_name_plural = _('newsletters')
+
+ def get_absolute_url(self):
+ return reverse('newsletter_detail', kwargs={'newsletter_slug': self.slug})
+
+ def subscribe_url(self):
+ return reverse('newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug})
+
+ def unsubscribe_url(self):
+ return reverse('newsletter_unsubscribe_request', kwargs={'newsletter_slug': self.slug})
+
+ def update_url(self):
+ return reverse('newsletter_update_request', kwargs={'newsletter_slug': self.slug})
+
+ def archive_url(self):
+ return reverse('newsletter_archive', kwargs={'newsletter_slug': self.slug})
+
+ def get_sender(self):
+ return get_address(self.sender, self.email)
+
+ def get_subscriptions(self):
+ logger.debug(u'Looking up subscribers for %s', self)
+
+ return Subscription.objects.filter(newsletter=self, subscribed=True)
+
+ @classmethod
+ def get_default(cls):
+ try:
+ return cls.objects.all()[0].pk
+ except IndexError:
+ return None
+
+
+@python_2_unicode_compatible
+class Subscription(models.Model):
+ user = models.ForeignKey(
+ AUTH_USER_MODEL, blank=True, null=True, verbose_name=_('user'),
+ on_delete=models.CASCADE
+ )
+
+ name_field = models.CharField(
+ db_column='name', max_length=30, blank=True, null=True,
+ verbose_name=_('name'), help_text=_('optional')
+ )
+
+ def get_name(self):
+ if self.user:
+ return self.user.get_full_name()
+ return self.name_field
+
+ def set_name(self, name):
+ if not self.user:
+ self.name_field = name
+ name = property(get_name, set_name)
+
+ email_field = models.EmailField(
+ db_column='email', verbose_name=_('e-mail'), db_index=True,
+ blank=True, null=True
+ )
+
+ def get_email(self):
+ if self.user:
+ return self.user.email
+ return self.email_field
+
+ def set_email(self, email):
+ if not self.user:
+ self.email_field = email
+ email = property(get_email, set_email)
+
+ def update(self, action):
+ """
+ Update subscription according to requested action:
+ subscribe/unsubscribe/update/, then save the changes.
+ """
+
+ assert action in ('subscribe', 'update', 'unsubscribe')
+
+ # If a new subscription or update, make sure it is subscribed
+ # Else, unsubscribe
+ if action == 'subscribe' or action == 'update':
+ self.subscribed = True
+ else:
+ self.unsubscribed = True
+
+ logger.debug(
+ _(u'Updated subscription %(subscription)s to %(action)s.'),
+ {
+ 'subscription': self,
+ 'action': action
+ }
+ )
+
+ # This triggers the subscribe() and/or unsubscribe() methods, taking
+ # care of stuff like maintaining the (un)subscribe date.
+ self.save()
+
+ def _subscribe(self):
+ """
+ Internal helper method for managing subscription state
+ during subscription.
+ """
+ logger.debug(u'Subscribing subscription %s.', self)
+
+ self.subscribe_date = now()
+ self.subscribed = True
+ self.unsubscribed = False
+
+ def _unsubscribe(self):
+ """
+ Internal helper method for managing subscription state
+ during unsubscription.
+ """
+ logger.debug(u'Unsubscribing subscription %s.', self)
+
+ self.subscribed = False
+ self.unsubscribed = True
+ self.unsubscribe_date = now()
+
+ def save(self, *args, **kwargs):
+ """
+ Perform some basic validation and state maintenance of Subscription.
+ TODO: Move this code to a more suitable place (i.e. `clean()`) and
+ cleanup the code. Refer to comment below and
+ https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
+ """
+ assert self.user or self.email_field, \
+ _('Neither an email nor a username is set. This asks for '
+ 'inconsistency!')
+ assert ((self.user and not self.email_field) or
+ (self.email_field and not self.user)), \
+ _('If user is set, email must be null and vice versa.')
+
+ # This is a lame way to find out if we have changed but using Django
+ # API internals is bad practice. This is necessary to discriminate
+ # from a state where we have never been subscribed but is mostly for
+ # backward compatibility. It might be very useful to make this just
+ # one attribute 'subscribe' later. In this case unsubscribed can be
+ # replaced by a method property.
+
+ if self.pk:
+ assert(Subscription.objects.filter(pk=self.pk).count() == 1)
+
+ subscription = Subscription.objects.get(pk=self.pk)
+ old_subscribed = subscription.subscribed
+ old_unsubscribed = subscription.unsubscribed
+
+ # If we are subscribed now and we used not to be so, subscribe.
+ # If we user to be unsubscribed but are not so anymore, subscribe.
+ if ((self.subscribed and not old_subscribed) or
+ (old_unsubscribed and not self.unsubscribed)):
+ self._subscribe()
+
+ assert not self.unsubscribed
+ assert self.subscribed
+
+ # If we are unsubcribed now and we used not to be so, unsubscribe.
+ # If we used to be subscribed but are not subscribed anymore,
+ # unsubscribe.
+ elif ((self.unsubscribed and not old_unsubscribed) or
+ (old_subscribed and not self.subscribed)):
+ self._unsubscribe()
+
+ assert not self.subscribed
+ assert self.unsubscribed
+ else:
+ if self.subscribed:
+ self._subscribe()
+ elif self.unsubscribed:
+ self._unsubscribe()
+
+ super(Subscription, self).save(*args, **kwargs)
+
+ ip = models.GenericIPAddressField(_("IP address"), blank=True, null=True)
+
+ newsletter = models.ForeignKey(
+ Newsletter, verbose_name=_('newsletter'), on_delete=models.CASCADE
+ )
+
+ create_date = models.DateTimeField(editable=False, default=now)
+
+ activation_code = models.CharField(
+ verbose_name=_('activation code'), max_length=40,
+ default=make_activation_code
+ )
+
+ subscribed = models.BooleanField(
+ default=False, verbose_name=_('subscribed'), db_index=True
+ )
+ subscribe_date = models.DateTimeField(
+ verbose_name=_("subscribe date"), null=True, blank=True
+ )
+
+ # This should be a pseudo-field, I reckon.
+ unsubscribed = models.BooleanField(
+ default=False, verbose_name=_('unsubscribed'), db_index=True
+ )
+ unsubscribe_date = models.DateTimeField(
+ verbose_name=_("unsubscribe date"), null=True, blank=True
+ )
+
+ def __str__(self):
+ if self.name:
+ return _(u"%(name)s <%(email)s> to %(newsletter)s") % {
+ 'name': self.name,
+ 'email': self.email,
+ 'newsletter': self.newsletter
+ }
+
+ else:
+ return _(u"%(email)s to %(newsletter)s") % {
+ 'email': self.email,
+ 'newsletter': self.newsletter
+ }
+
+ class Meta:
+ verbose_name = _('subscription')
+ verbose_name_plural = _('subscriptions')
+ unique_together = ('user', 'email_field', 'newsletter')
+
+ def get_recipient(self):
+ return get_address(self.name, self.email)
+
+ def send_activation_email(self, action):
+ assert action in ACTIONS, 'Unknown action: %s' % action
+
+ (subject_template, text_template, html_template) = \
+ self.newsletter.get_templates(action)
+
+ variable_dict = {
+ 'subscription': self,
+ 'site': Site.objects.get_current(),
+ 'newsletter': self.newsletter,
+ 'date': self.subscribe_date,
+ 'STATIC_URL': settings.STATIC_URL,
+ 'MEDIA_URL': settings.MEDIA_URL
+ }
+
+ unescaped_context = get_context(variable_dict, autoescape=False)
+
+ subject = subject_template.render(unescaped_context).strip()
+ text = text_template.render(unescaped_context)
+
+ message = EmailMultiAlternatives(
+ subject, text,
+ from_email=self.newsletter.get_sender(),
+ to=[self.email]
+ )
+
+ if html_template:
+ escaped_context = get_context(variable_dict)
+
+ message.attach_alternative(
+ html_template.render(escaped_context), "text/html"
+ )
+
+ message.send()
+
+ logger.debug(
+ u'Activation email sent for action "%(action)s" to %(subscriber)s '
+ u'with activation code "%(action_code)s".', {
+ 'action_code': self.activation_code,
+ 'action': action,
+ 'subscriber': self
+ }
+ )
+
+ def subscribe_activate_url(self):
+ return reverse('newsletter_update_activate', kwargs={
+ 'newsletter_slug': self.newsletter.slug,
+ 'email': self.email,
+ 'action': 'subscribe',
+ 'activation_code': self.activation_code
+ })
+
+ def unsubscribe_activate_url(self):
+ return reverse('newsletter_update_activate', kwargs={
+ 'newsletter_slug': self.newsletter.slug,
+ 'email': self.email,
+ 'action': 'unsubscribe',
+ 'activation_code': self.activation_code
+ })
+
+ def update_activate_url(self):
+ return reverse('newsletter_update_activate', kwargs={
+ 'newsletter_slug': self.newsletter.slug,
+ 'email': self.email,
+ 'action': 'update',
+ 'activation_code': self.activation_code
+ })
+
+
+@python_2_unicode_compatible
+class Article(models.Model):
+ """
+ An Article within a Message which will be send through a Submission.
+ """
+
+ sortorder = models.PositiveIntegerField(
+ help_text=_('Sort order determines the order in which articles are '
+ 'concatenated in a post.'),
+ verbose_name=_('sort order'), blank=True
+ )
+
+ title = models.CharField(max_length=200, verbose_name=_('title'))
+ text = models.TextField(verbose_name=_('text'))
+
+ url = models.URLField(
+ verbose_name=_('link'), blank=True, null=True
+ )
+
+ # Make this a foreign key for added elegance
+ image = ImageField(
+ upload_to='newsletter/images/%Y/%m/%d', blank=True, null=True,
+ verbose_name=_('image')
+ )
+
+ # Message this article is associated with
+ # TODO: Refactor post to message (post is legacy notation).
+ post = models.ForeignKey(
+ 'Message', verbose_name=_('message'), related_name='articles',
+ on_delete=models.CASCADE
+ )
+
+ class Meta:
+ ordering = ('sortorder',)
+ verbose_name = _('article')
+ verbose_name_plural = _('articles')
+ unique_together = ('post', 'sortorder')
+
+ def __str__(self):
+ return self.title
+
+ def save(self, **kwargs):
+ if self.sortorder is None:
+ # If saving a new object get the next available Article ordering
+ # as to assure uniqueness.
+ self.sortorder = self.post.get_next_article_sortorder()
+
+ super(Article, self).save()
+
+
+def get_default_newsletter():
+ return Newsletter.get_default()
+
+@python_2_unicode_compatible
+class Message(models.Model):
+ """ Message as sent through a Submission. """
+
+ title = models.CharField(max_length=200, verbose_name=_('title'))
+ slug = models.SlugField(verbose_name=_('slug'))
+
+ newsletter = models.ForeignKey(
+ Newsletter, verbose_name=_('newsletter'), on_delete=models.CASCADE, default=get_default_newsletter
+ )
+
+ date_create = models.DateTimeField(
+ verbose_name=_('created'), auto_now_add=True, editable=False
+ )
+ date_modify = models.DateTimeField(
+ verbose_name=_('modified'), auto_now=True, editable=False
+ )
+
+ class Meta:
+ verbose_name = _('message')
+ verbose_name_plural = _('messages')
+ unique_together = ('slug', 'newsletter')
+
+ def __str__(self):
+ try:
+ return _(u"%(title)s in %(newsletter)s") % {
+ 'title': self.title,
+ 'newsletter': self.newsletter
+ }
+ except Newsletter.DoesNotExist:
+ logger.warning('No newsletter has been set for this message yet.')
+ return self.title
+
+ def get_next_article_sortorder(self):
+ """ Get next available sortorder for Article. """
+
+ next_order = self.articles.aggregate(
+ models.Max('sortorder')
+ )['sortorder__max']
+
+ if next_order:
+ return next_order + 10
+ else:
+ return 10
+
+ @cached_property
+ def _templates(self):
+ """Return a (subject_template, text_template, html_template) tuple."""
+ return self.newsletter.get_templates('message')
+
+ @property
+ def subject_template(self):
+ return self._templates[0]
+
+ @property
+ def text_template(self):
+ return self._templates[1]
+
+ @property
+ def html_template(self):
+ return self._templates[2]
+
+ @classmethod
+ def get_default(cls):
+ try:
+ return cls.objects.order_by('-date_create').all()[0]
+ except IndexError:
+ return None
+
+
+@python_2_unicode_compatible
+class Submission(models.Model):
+ """
+ Submission represents a particular Message as it is being submitted
+ to a list of Subscribers. This is where actual queueing and submission
+ happen.
+ """
+ class Meta:
+ verbose_name = _('submission')
+ verbose_name_plural = _('submissions')
+
+ def __str__(self):
+ return _(u"%(newsletter)s on %(publish_date)s") % {
+ 'newsletter': self.message,
+ 'publish_date': self.publish_date
+ }
+
+ @cached_property
+ def extra_headers(self):
+ return {
+ 'List-Unsubscribe': 'http://%s%s' % (
+ Site.objects.get_current().domain,
+ reverse('newsletter_unsubscribe_request',
+ args=[self.message.newsletter.slug])
+ ),
+ }
+
+ def submit(self):
+ subscriptions = self.subscriptions.filter(subscribed=True)
+
+ logger.info(
+ ugettext(u"Submitting %(submission)s to %(count)d people"),
+ {'submission': self, 'count': subscriptions.count()}
+ )
+
+ assert self.publish_date < now(), \
+ 'Something smells fishy; submission time in future.'
+
+ self.sending = True
+ self.save()
+
+ try:
+ for idx, subscription in enumerate(subscriptions, start=1):
+ if hasattr(settings, 'NEWSLETTER_EMAIL_DELAY'):
+ time.sleep(settings.NEWSLETTER_EMAIL_DELAY)
+ if hasattr(settings, 'NEWSLETTER_BATCH_SIZE') and settings.NEWSLETTER_BATCH_SIZE > 0:
+ if idx % settings.NEWSLETTER_BATCH_SIZE == 0:
+ time.sleep(settings.NEWSLETTER_BATCH_DELAY)
+ self.send_message(subscription)
+ self.sent = True
+
+ finally:
+ self.sending = False
+ self.save()
+
+ def send_message(self, subscription):
+ variable_dict = {
+ 'subscription': subscription,
+ 'site': Site.objects.get_current(),
+ 'submission': self,
+ 'message': self.message,
+ 'newsletter': self.newsletter,
+ 'date': self.publish_date,
+ 'STATIC_URL': settings.STATIC_URL,
+ 'MEDIA_URL': settings.MEDIA_URL
+ }
+
+ unescaped_context = get_context(variable_dict, autoescape=False)
+
+ subject = self.message.subject_template.render(
+ unescaped_context).strip()
+ text = self.message.text_template.render(unescaped_context)
+
+ message = EmailMultiAlternatives(
+ subject, text,
+ from_email=self.newsletter.get_sender(),
+ to=[subscription.get_recipient()],
+ headers=self.extra_headers,
+ )
+
+ if self.message.html_template:
+ escaped_context = get_context(variable_dict)
+
+ message.attach_alternative(
+ self.message.html_template.render(escaped_context),
+ "text/html"
+ )
+
+ try:
+ logger.debug(
+ ugettext(u'Submitting message to: %s.'),
+ subscription
+ )
+
+ message.send()
+
+ except Exception as e:
+ # TODO: Test coverage for this branch.
+ logger.error(
+ ugettext(u'Message %(subscription)s failed '
+ u'with error: %(error)s'),
+ {'subscription': subscription,
+ 'error': e}
+ )
+
+ @classmethod
+ def submit_queue(cls):
+ todo = cls.objects.filter(
+ prepared=True, sent=False, sending=False,
+ publish_date__lt=now()
+ )
+
+ for submission in todo:
+ submission.submit()
+
+ @classmethod
+ def from_message(cls, message):
+ logger.debug(ugettext('Submission of message %s'), message)
+ submission = cls()
+ submission.message = message
+ submission.newsletter = message.newsletter
+ submission.save()
+ try:
+ submission.subscriptions.set(message.newsletter.get_subscriptions())
+ except AttributeError: # Django < 1.10
+ submission.subscriptions = message.newsletter.get_subscriptions()
+ return submission
+
+ def save(self, **kwargs):
+ """ Set the newsletter from associated message upon saving. """
+ assert self.message.newsletter
+
+ self.newsletter = self.message.newsletter
+
+ return super(Submission, self).save()
+
+
+
+ def get_absolute_url(self):
+ assert self.newsletter.slug
+ assert self.message.slug
+
+ return reverse(
+ 'newsletter_archive_detail', kwargs={
+ 'newsletter_slug': self.newsletter.slug,
+ 'year': self.publish_date.year,
+ 'month': self.publish_date.month,
+ 'day': self.publish_date.day,
+ 'slug': self.message.slug
+ }
+ )
+
+ newsletter = models.ForeignKey(
+ Newsletter, verbose_name=_('newsletter'), editable=False,
+ on_delete=models.CASCADE
+ )
+ message = models.ForeignKey(
+ Message, verbose_name=_('message'), editable=True, null=False,
+ on_delete=models.CASCADE
+ )
+
+ subscriptions = models.ManyToManyField(
+ 'Subscription',
+ help_text=_('If you select none, the system will automatically find '
+ 'the subscribers for you.'),
+ blank=True, db_index=True, verbose_name=_('recipients'),
+ limit_choices_to={'subscribed': True}
+ )
+
+ publish_date = models.DateTimeField(
+ verbose_name=_('publication date'), blank=True, null=True,
+ default=now, db_index=True
+ )
+ publish = models.BooleanField(
+ default=True, verbose_name=_('publish'),
+ help_text=_('Publish in archive.'), db_index=True
+ )
+
+ prepared = models.BooleanField(
+ default=False, verbose_name=_('prepared'),
+ db_index=True, editable=False
+ )
+ sent = models.BooleanField(
+ default=False, verbose_name=_('sent'),
+ db_index=True, editable=False
+ )
+ sending = models.BooleanField(
+ default=False, verbose_name=_('sending'),
+ db_index=True, editable=False
+ )
+
+def get_address(name, email):
+ # Converting name to ascii for compatibility with django < 1.9.
+ # Remove this when django 1.8 is no longer supported.
+ if LooseVersion(django.get_version()) < LooseVersion('1.9'):
+ name = name.encode('ascii', 'ignore').decode('ascii').strip()
+ if name:
+ return u'%s <%s>' % (name, email)
+ else:
+ return u'%s' % email
diff --git a/app/lttr/send.py b/app/lttr/send.py
new file mode 100644
index 0000000..cd012e2
--- /dev/null
+++ b/app/lttr/send.py
@@ -0,0 +1,6 @@
+from lttr.models import Newsletter, NewsletterMailing
+from lttr.mailer import SendShit
+n = Newsletter.objects.get(pk=tk)
+m = NewsletterMailing.objects.get(pk=tk)
+s = SendShit(n, m)
+s.send_mailings()
diff --git a/app/lttr/templates/lttr/confirm_activate.html b/app/lttr/templates/lttr/confirm_activate.html
new file mode 100644
index 0000000..a2d3a3d
--- /dev/null
+++ b/app/lttr/templates/lttr/confirm_activate.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+
+{% block pagetitle %}Your subscription is active, thank you! | luxagraf.net {% endblock %}
+{% block metadescription %}Thank you, I appreciate you joining the club{% endblock %}
+
+{% block primary %}
+ <nav class="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <span class="nav-item" itemprop="item">
+ <a href="/" itemprop="name">Home</a>
+ <meta itemprop="position" content="1" />
+ </span>
+ <span class="nav-item" itemprop="item">
+ <span itemprop="name">lttr</span>
+ <meta itemprop="position" content="2" />
+ </span>
+ </nav>
+ <main role="main" id="essay-archive" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2>You're confirmed, thanks for joining.</h2>
+ <p>If you'd like you can <a href="/{{newsletter}}/">browse the archives</a> of past mailings.</p>
+ </div>
+ </main>
+{%endblock%}
diff --git a/app/lttr/templates/lttr/emails/friends_html_email.html b/app/lttr/templates/lttr/emails/friends_html_email.html
new file mode 100644
index 0000000..b7e4bf2
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/friends_html_email.html
@@ -0,0 +1,249 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name=”robot” content=”noindex” style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title}}</title>
+
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+@font-face {
+ font-family: 'mffnweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffnbweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+.tk-ff-meta-web-pro { font-family: mffnbweb,sans-serif; }
+.tk-mffnweb { font-family: mffnweb,serif; }
+* {
+ margin:0;
+ padding:0;
+}
+* { }
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ max-width: 100%;
+}
+img.fullbleed { display: inline; border-radius: 3px; margin-bottom: 1.5em; width: 100% !important; max-width: 100% !important; height: auto !important; max-height: auto !important; }
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+body {
+ -webkit-font-smoothing:antialiased;
+ -webkit-text-size-adjust:none;
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+}
+a { color: #000; font-weight: 600; text-decoration: none; border-bottom: 1px solid #ddd; }
+.btn {
+ text-decoration:none;
+ color: #FFF;
+ background-color: #666;
+ padding:10px 16px;
+ font-weight:bold;
+ margin-right:10px;
+ text-align:center;
+ cursor:pointer;
+ display: inline-block;
+}
+p.callout {
+ padding:15px;
+ background-color:#ECF8FF;
+ margin-bottom: 15px;
+}
+.callout a {
+ font-weight:bold;
+ color: #2BA6CB;
+}
+.highlight { background-color: #ffffb2;}
+figure { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding-top: 20px; padding-bottom: 20px; margin-bottom: 30px; }
+figcaption { text-align: center; font-size: .8em; }
+.sp { font-size: .85em; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; text-decoration: none; }
+.footnotes ol li { font-size: .8em; }
+.footnotes ol li p { font-size: .8em; }
+.footnotes hr { display: none; }
+h1,h2,h3,h4,h5,h6 {
+font-family: mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: center;
+}
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; }
+h1 { font-weight:400; font-size: 44px;}
+h2 { font-weight:400; font-size: 30px;}
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:500; font-size: 23px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; text-align: left; }
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ margin-bottom: 1.4em;
+ font-weight: 400;
+
+ font-size:17px;
+ line-height:1.5;
+ hyphens: auto;
+}
+hr { width: 50%; margin: 40px auto; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+p.lead { font-size:17px; }
+p.last { margin-bottom:0px; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.container {
+ display:block!important;
+ max-width:720px!important;
+ margin:0 auto!important;
+ clear:both!important;
+}
+.content {
+ padding:17px;
+ max-width:720px;
+ margin:0 auto;
+ display:block;
+}
+.content table { width: 100%; }
+.clear { display: block; clear: both; }
+@media only screen and (max-width: 700px) {
+
+ a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;}
+ img.fullbleed { margin-bottom: 1.5em; width: 100%; height: auto !important; }
+ p { font-size: 16px;}
+ h1 { font-size: 36px; }
+ div[class="column"] { width: auto!important; float:none!important;}
+
+ table.social div[class="column"] {
+ width:auto!important;
+ }
+}
+
+</style>
+
+</head>
+<body style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;background-color:#ffffff;height:100%;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <h2 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:30px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" ><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <span style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-size:.5em;line-height:2em;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ><a href="https://luxagraf.net/friends/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#000;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-weight:normal;" >Friends of a Long Year</a> — {{object.get_issue_str}} — {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></singleline></span></h2>
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:44px;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title|safe|smartypants}}</singleline></h1>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <a href="https://luxagraf.net{{object.post.get_absolute_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-width:0;color:#000;font-weight:600;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;" >
+ {% include "lib/friends_featured_img.html" with image=object.featured_image %}
+ </a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ {{object.body_email_html|safe|smartypants}}
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ </td>
+ </tr>
+ </table>
+ </div>
+</td>
+<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+</tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td align="center" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >New subscriber?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ Browse the <a href="https://luxagraf.net/jrnl/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >online archives</a> here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Friends</em>?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ A monthly letter from <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >Scott Gilberson</a>, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />also known as luxagraf..<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >
+ Shipped from Points Unknown, USA.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ Explained <a href="https://luxagraf.net/jrnl/2020/11/invitation" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >here</a>.</p>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >If you enjoy this, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ please consider forwarding it to a friend. <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />We are after all, friends of a long year here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ </tr>
+</table>
+</html>
+
+
diff --git a/app/lttr/templates/lttr/emails/friends_plain_text_email.txt b/app/lttr/templates/lttr/emails/friends_plain_text_email.txt
new file mode 100644
index 0000000..9ff7f61
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/friends_plain_text_email.txt
@@ -0,0 +1,16 @@
+
+{{ object.email_encode|safe }}
+
+-----
+
+You're getting this email because you signed up for
+
+Scott Gilbertson's (luxagraf)[https://luxagraf.net/] newsletter,
+
+*Friends of a Long Year* [https://luxagraf.net/friends/]
+
+If you're new, you can explore past posts here: [https://luxagraf.net/jrnl/]
+
+You can always: Unsubscribe [https://luxagraf.net{{subscriber.unsubscribe_activate_url}}] instantly.
+
+[https://luxagraf.net/] ✪ [https://luxagraf.net/friends/]
diff --git a/app/lttr/templates/lttr/emails/range_html_email.html b/app/lttr/templates/lttr/emails/range_html_email.html
new file mode 100644
index 0000000..09c2db3
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/range_html_email.html
@@ -0,0 +1,239 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name=”robot” content=”noindex” style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title}}</title>
+
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+@font-face {
+ font-family: 'mffnweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffnbweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+.tk-ff-meta-web-pro { font-family: mffnbweb,sans-serif; }
+.tk-mffnweb { font-family: mffnweb,serif; }
+* {
+ margin:0;
+ padding:0;
+}
+* { }
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ width: 846px;
+}
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+body {
+ -webkit-font-smoothing:antialiased;
+ -webkit-text-size-adjust:none;
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+}
+a { color: #000; font-weight: 600; text-decoration: none; border-bottom: 1px solid #ddd; }
+.btn {
+ text-decoration:none;
+ color: #FFF;
+ background-color: #666;
+ padding:10px 16px;
+ font-weight:bold;
+ margin-right:10px;
+ text-align:center;
+ cursor:pointer;
+ display: inline-block;
+}
+p.callout {
+ padding:15px;
+ background-color:#ECF8FF;
+ margin-bottom: 15px;
+}
+.callout a {
+ font-weight:bold;
+ color: #2BA6CB;
+}
+.highlight { background-color: #ffffb2;}
+figure { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding-top: 20px; padding-bottom: 20px; margin-bottom: 30px; }
+figcaption { text-align: center; font-size: .8em; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; text-decoration: none; }
+.footnotes ol li { font-size: .8em; }
+.footnotes ol li p { font-size: .8em; }
+.footnotes hr { display: none; }
+h1,h2,h3,h4,h5,h6 {
+font-family: mffnweb, 'Lucida Serif', Georgia, serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: center;
+}
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; }
+h1 { font-weight:400; font-size: 44px;}
+h2 { font-weight:400; font-size: 30px;}
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:500; font-size: 23px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; text-align: left; }
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ margin-bottom: 1.4em;
+ font-weight: 400;
+
+ font-size:17px;
+ line-height:1.5;
+ hyphens: auto;
+}
+hr { width: 50%; margin: 40px auto; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.container {
+ display:block!important;
+ max-width:960px!important;
+ margin:0 auto!important;
+ clear:both!important;
+}
+.content {
+ padding:17px;
+ max-width:960px;
+ margin:0 auto;
+ display:block;
+}
+.content table { width: 100%; }
+.clear { display: block; clear: both; }
+@media only screen and (max-width: 700px) {
+
+ a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;}
+ p { font-size: 16px;}
+ h1 { font-size: 36px; }
+ div[class="column"] { width: auto!important; float:none!important;}
+
+}
+
+</style>
+
+</head>
+<body style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;background-color:#ffffff;height:100%;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:960px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:960px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <h2 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:30px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" ><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <span style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-size:.5em;line-height:2em;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ><a href="https://luxagraf.net/range/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#000;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-weight:normal;" >✪ Range</a> — {{object.get_issue_str}} — {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></singleline></span></h2>
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:44px;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title|safe|smartypants}}</singleline></h1>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <a href="https://luxagraf.net{{object.post.get_absolute_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-width:0;color:#000;font-weight:600;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;" >
+ {% include "lib/friends_featured_img.html" with image=object.featured_image %}
+ </a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ If you'd like to view a larger version, read some backstory, and see a video of the development process in Darktable, head on over to: <a href="https://luxagraf.net{{object.post.get_absolute_url}}">https://luxagraf.net{{object.post.get_absolute_url}}</a>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ </td>
+ </tr>
+ </table>
+ </div>
+</td>
+<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+</tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:960px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:960px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td align="center" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >New subscriber?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ Browse the <a href="https://luxagraf.net/range/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >online archives</a> here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Range</em>?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ A weekly letter from <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >Scott Gilberson</a>, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />also known as luxagraf.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+
+<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >
+ Shipped from Points Unknown, USA.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ Explained <a href="https://luxagraf.net/range" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >here</a>.</p>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >If you enjoy this, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ please consider forwarding it to a friend.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >✪ </p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ </tr>
+</table>
+</html>
+
+
diff --git a/app/lttr/templates/lttr/emails/range_plain_text_email.txt b/app/lttr/templates/lttr/emails/range_plain_text_email.txt
new file mode 100644
index 0000000..2044e44
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/range_plain_text_email.txt
@@ -0,0 +1,19 @@
+Greetings Range subscribers-
+
+I respect your desire for plain text email, but there really isn't a way to do a photo newsletter in plain text, except to say, here's a link to the web-based version:
+
+<https://luxagraf.net{{ object.get_absolute_url }}>
+
+-----
+
+You're getting this email because you signed up for
+
+Scott Gilbertson's (luxagraf)[https://luxagraf.net/] photo newsletter,
+
+*Range* [https://luxagraf.net/range/]
+
+If you're new, you can explore past letters here: [https://luxagraf.net/range/]
+
+You can always: Unsubscribe [https://luxagraf.net{{subscriber.unsubscribe_activate_url}}] instantly.
+
+[https://luxagraf.net/] ✪ [https://luxagraf.net/range/]
diff --git a/app/lttr/templates/lttr/emails/test-friends_html_email.html b/app/lttr/templates/lttr/emails/test-friends_html_email.html
new file mode 100644
index 0000000..d1e2f5a
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/test-friends_html_email.html
@@ -0,0 +1,250 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name=”robot” content=”noindex” style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title}}</title>
+
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+@font-face {
+ font-family: 'mffnweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffnbweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+.tk-ff-meta-web-pro { font-family: mffnbweb,sans-serif; }
+.tk-mffnweb { font-family: mffnweb,serif; }
+* {
+ margin:0;
+ padding:0;
+}
+* { }
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ max-width: 100%;
+}
+img.fullbleed { display: inline; border-radius: 3px; margin-bottom: 1.5em; width: 100% !important; max-width: 100% !important; height: auto !important; max-height: auto !important; }
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+body {
+ -webkit-font-smoothing:antialiased;
+ -webkit-text-size-adjust:none;
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+}
+a { color: #000; font-weight: 600; text-decoration: none; border-bottom: 1px solid #ddd; }
+.btn {
+ text-decoration:none;
+ color: #FFF;
+ background-color: #666;
+ padding:10px 16px;
+ font-weight:bold;
+ margin-right:10px;
+ text-align:center;
+ cursor:pointer;
+ display: inline-block;
+}
+p.callout {
+ padding:15px;
+ background-color:#ECF8FF;
+ margin-bottom: 15px;
+}
+.callout a {
+ font-weight:bold;
+ color: #2BA6CB;
+}
+.highlight { background-color: #ffffb2;}
+figure { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding-top: 20px; padding-bottom: 20px; margin-bottom: 30px; }
+figcaption { text-align: center; font-size: .8em; }
+.sp { font-size: .85em; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; text-decoration: none; }
+.footnotes ol li { font-size: .8em; }
+.footnotes ol li p { font-size: .8em; }
+.footnotes hr { display: none; }
+h1,h2,h3,h4,h5,h6 {
+font-family: mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: center;
+}
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; }
+h1 { font-weight:400; font-size: 44px;}
+h2 { font-weight:400; font-size: 30px;}
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:500; font-size: 23px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; text-align: left; }
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ margin-bottom: 1.4em;
+ font-weight: 400;
+
+ font-size:17px;
+ line-height:1.5;
+ hyphens: auto;
+}
+hr { width: 50%; margin: 40px auto; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+p.lead { font-size:17px; }
+p.last { margin-bottom:0px; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.container {
+ display:block!important;
+ max-width:720px!important;
+ margin:0 auto!important;
+ clear:both!important;
+}
+.content {
+ padding:17px;
+ max-width:720px;
+ margin:0 auto;
+ display:block;
+}
+.content table { width: 100%; }
+.clear { display: block; clear: both; }
+@media only screen and (max-width: 700px) {
+
+ a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;}
+ img.fullbleed { margin-bottom: 1.5em; width: 100%; height: auto !important; }
+ p { font-size: 16px;}
+ h1 { font-size: 36px; }
+ div[class="column"] { width: auto!important; float:none!important;}
+
+ table.social div[class="column"] {
+ width:auto!important;
+ }
+}
+
+</style>
+
+</head>
+<body style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;background-color:#ffffff;height:100%;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <h2 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:30px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" ><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <span style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-size:.5em;line-height:2em;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ><a href="https://luxagraf.net/friends/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#000;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-weight:normal;" >Friends of a Long Year</a> — {{object.get_issue_str}} — {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></singleline></span></h2>
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:44px;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title|safe|smartypants}}</singleline></h1>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <a href="https://luxagraf.net{{object.post.get_absolute_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-width:0;color:#000;font-weight:600;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;" >
+ {% include "lib/friends_featured_img.html" with image=object.featured_image %}
+ </a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ {{object.body_email_html|safe|smartypants}}
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ </td>
+ </tr>
+ </table>
+ </div>
+</td>
+<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+</tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:720px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:720px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td align="center" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >New subscriber?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ Browse the <a href="https://luxagraf.net/friends/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >online archives</a> here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Friends</em>?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ A monthly letter from <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >Scott Gilberson</a>, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />also known as luxagraf..<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >
+ Shipped from Points Unknown, USA.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ Explained <a href="https://luxagraf.net/jrnl/2020/11/invitation" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >here</a>.</p>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >If you enjoy this, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ please consider forwarding it to a friend. <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />We are after all, friends of a long year here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >⫹⫺</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ </tr>
+</table>
+</html>
+
+
diff --git a/app/lttr/templates/lttr/emails/test-friends_plain_text_email.txt b/app/lttr/templates/lttr/emails/test-friends_plain_text_email.txt
new file mode 100644
index 0000000..286b527
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/test-friends_plain_text_email.txt
@@ -0,0 +1,16 @@
+
+{{ object.email_encode|safe }}
+
+-----
+
+You're getting this email because you signed up for
+
+Scott Gilbertson's (luxagraf)[https://luxagraf.net/] newsletter,
+
+*Friends of a Long Year* [https://luxagraf.net/friends/]
+
+If you're new, you can explore past letters here: [https://luxagraf.net/friends/]
+
+You can always: Unsubscribe [https://luxagraf.net{{subscriber.unsubscribe_activate_url}}] instantly.
+
+[https://luxagraf.net/] ✪ [https://luxagraf.net/friends/]
diff --git a/app/lttr/templates/lttr/emails/test-range_html_email.html b/app/lttr/templates/lttr/emails/test-range_html_email.html
new file mode 100644
index 0000000..d11479c
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/test-range_html_email.html
@@ -0,0 +1,239 @@
+{% load typogrify_tags %}
+<!DOCTYPE html>
+<html lang="en" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta name="viewport" content="width=device-width" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <meta name=”robot” content=”noindex” style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <title style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title}}</title>
+
+ <style type="text/css" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+@font-face {
+ font-family: 'mffnweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffnbweb';
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmn.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('https://luxagraf.net/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+.tk-ff-meta-web-pro { font-family: mffnbweb,sans-serif; }
+.tk-mffnweb { font-family: mffnweb,serif; }
+* {
+ margin:0;
+ padding:0;
+}
+* { }
+sup, sub {
+ vertical-align: baseline;
+ position: relative;
+ top: -0.4em;
+}
+sub {
+ top: 0.4em;
+}
+img {
+ width: 846px;
+}
+p img.fullbleed { margin-bottom: 0px; }
+.collapse {
+ margin:0;
+ padding:0;
+}
+body {
+ -webkit-font-smoothing:antialiased;
+ -webkit-text-size-adjust:none;
+ width: 100%!important;
+ background-color: #ffffff;
+ height: 100%;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
+}
+a { color: #000; font-weight: 600; text-decoration: none; border-bottom: 1px solid #ddd; }
+.btn {
+ text-decoration:none;
+ color: #FFF;
+ background-color: #666;
+ padding:10px 16px;
+ font-weight:bold;
+ margin-right:10px;
+ text-align:center;
+ cursor:pointer;
+ display: inline-block;
+}
+p.callout {
+ padding:15px;
+ background-color:#ECF8FF;
+ margin-bottom: 15px;
+}
+.callout a {
+ font-weight:bold;
+ color: #2BA6CB;
+}
+.highlight { background-color: #ffffb2;}
+figure { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding-top: 20px; padding-bottom: 20px; margin-bottom: 30px; }
+figcaption { text-align: center; font-size: .8em; }
+table.head-wrap { width: 100%;}
+table.body-wrap { width: 100%;}
+table.footer-wrap {
+ width: 100%;
+ clear:both!important;
+ color: #999;
+ font-family: helvetica !important;
+ font-size: 10px !important;
+}
+.footer-wrap .container .content p {
+ font-size: 14px;
+}
+.footer-wrap .container .content a { color: #333; text-decoration: none; }
+.footnotes ol li { font-size: .8em; }
+.footnotes ol li p { font-size: .8em; }
+.footnotes hr { display: none; }
+h1,h2,h3,h4,h5,h6 {
+font-family: mffnweb, 'Lucida Serif', Georgia, serif;
+line-height: 1;
+margin-bottom:15px;
+color:#000;
+text-align: center;
+}
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; }
+h1 { font-weight:400; font-size: 44px;}
+h2 { font-weight:400; font-size: 30px;}
+h4 { font-weight:500; font-size: 23px;}
+h5 { font-weight:500; font-size: 23px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; text-align: left; }
+h3, h6 { font-weight:400; font-size: 32px; font-style: italic; margin-top: 40px; text-transform: none; color:#000; text-align: left;}
+h2 { font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; }
+h2 a { font-weight: normal; }
+.collapse { margin:0!important;}
+p, ul, ol {
+ margin-bottom: 1.4em;
+ font-weight: 400;
+
+ font-size:17px;
+ line-height:1.5;
+ hyphens: auto;
+}
+hr { width: 50%; margin: 40px auto; border: 0; border-top: 1px solid #ddd; }
+p.quote { padding-left: 10px; border-left: 2px solid #ddd; }
+blockquote { border-left: 4px solid #efefef; padding-left: 15px; font-style: italic; }
+ul li, ol li {
+ margin-left: 35px;
+ list-style-position: outside;
+}
+.container {
+ display:block!important;
+ max-width:960px!important;
+ margin:0 auto!important;
+ clear:both!important;
+}
+.content {
+ padding:17px;
+ max-width:960px;
+ margin:0 auto;
+ display:block;
+}
+.content table { width: 100%; }
+.clear { display: block; clear: both; }
+@media only screen and (max-width: 700px) {
+
+ a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;}
+ p { font-size: 16px;}
+ h1 { font-size: 36px; }
+ div[class="column"] { width: auto!important; float:none!important;}
+
+}
+
+</style>
+
+</head>
+<body style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;background-color:#ffffff;height:100%;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" >
+ <table class="body-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:960px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:960px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <h2 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:30px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;" ><br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <span style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-size:.5em;line-height:2em;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ><a href="https://luxagraf.net/range/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#000;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;font-weight:normal;" >✪ Range</a> — {{object.get_issue_str}} — {{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></singleline></span></h2>
+ <h1 style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:mffnweb, 'Lora', 'Lucida Serif', Lucida, Georgia, serif;line-height:1;margin-bottom:15px;color:#000;text-align:center;font-weight:400;font-size:44px;" ><singleline style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >{{object.title|safe|smartypants}}</singleline></h1>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ <a href="https://luxagraf.net{{object.post.get_absolute_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;border-width:0;color:#000;font-weight:600;text-decoration:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;" >
+ {% include "lib/friends_featured_img.html" with image=object.featured_image %}
+ </a>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ If you'd like to view a larger version, read some backstory, and see a video of the development process in Darktable, head on over to: <a href="https://luxagraf.net{{object.post.get_absolute_url}}">https://luxagraf.net{{object.post.get_absolute_url}}</a>
+
+ <hr style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:50%;margin-top:40px;margin-bottom:40px;margin-right:auto;margin-left:auto;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" />
+ </td>
+ </tr>
+ </table>
+ </div>
+</td>
+<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+</tr>
+</table>
+<table class="footer-wrap" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;clear:both!important;color:#999;font-family:helvetica !important;font-size:10px !important;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ <td class="container" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:block!important;max-width:960px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;" >
+
+
+ <div class="content" style="padding-top:15px;padding-bottom:15px;padding-right:15px;padding-left:15px;max-width:960px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;" >
+ <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:100%;" >
+ <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <td align="center" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >New subscriber?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ Browse the <a href="https://luxagraf.net/range/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >online archives</a> here.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" ><em style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >Range</em>?<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+
+ A weekly letter from <a href="https://luxagraf.net/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >Scott Gilberson</a>, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />also known as luxagraf.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" /></p>
+
+<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >
+ Shipped from Points Unknown, USA.<br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ Explained <a href="https://luxagraf.net/range/" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#ddd;color:#333;text-decoration:none;" >here</a>.</p>
+ <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >If you enjoy this, <br style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" />
+ please consider forwarding it to a friend.</p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;margin-bottom:1.4em;font-weight:400;line-height:1.5;hyphens:auto;font-size:14px;" >✪ </p>
+ <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;line-height:1.5;hyphens:auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;color:#999;font-size:14px;" >You can always: <a href="https://luxagraf.net{{subscriber.unsubscribe_activate_url}}" style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:600;text-decoration:underline;color:#999 !important;" >Unsubscribe</a> instantly.</p>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" ></td>
+ </tr>
+</table>
+</html>
+
+
diff --git a/app/lttr/templates/lttr/emails/test-range_plain_text_email.txt b/app/lttr/templates/lttr/emails/test-range_plain_text_email.txt
new file mode 100644
index 0000000..970c42f
--- /dev/null
+++ b/app/lttr/templates/lttr/emails/test-range_plain_text_email.txt
@@ -0,0 +1,19 @@
+Greetings Range subscribers-
+
+While I respect your desire for plain text email, there really isn't a way to do a photo newsletter in plain text other than to say, here's a link to the web-based version:
+
+<https://luxagraf.net{{ object.get_absolute_url }}>
+
+-----
+
+You're getting this email because you signed up for
+
+Scott Gilbertson's (luxagraf)[https://luxagraf.net/] photo newsletter,
+
+*Range* [https://luxagraf.net/range/]
+
+If you're new, you can explore past letters here: [https://luxagraf.net/range/]
+
+You can always: Unsubscribe [https://luxagraf.net{{subscriber.unsubscribe_activate_url}}] instantly.
+
+[https://luxagraf.net/] ✪ [https://luxagraf.net/range/]
diff --git a/app/lttr/templates/lttr/message/subscribe.html b/app/lttr/templates/lttr/message/subscribe.html
new file mode 100644
index 0000000..56ccbcb
--- /dev/null
+++ b/app/lttr/templates/lttr/message/subscribe.html
@@ -0,0 +1,23 @@
+{% load i18n %}<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>{% blocktrans with title=newsletter.title %}Subscription to {{ title }}{% endblocktrans %}
+</title>
+</head>
+<body>
+{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.subscribe_activate_url intro=newsletter.intro %}
+
+<p>Hola-</p>
+
+<p>Someone, hopefully you, asked to subscribe to luxagraf's {{ title }} newsletter. {{ intro }}</p>
+
+<p>Please click this link to active your subscription:</p>
+
+https://{{ domain }}{{ url }}
+{% endblocktrans %}
+<p>cheers<br />
+Scott</p>
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/message/subscribe.txt b/app/lttr/templates/lttr/message/subscribe.txt
new file mode 100644
index 0000000..edbc467
--- /dev/null
+++ b/app/lttr/templates/lttr/message/subscribe.txt
@@ -0,0 +1,9 @@
+Hola-
+
+Someone, hopefully you, asked to subscribe to luxagraf's {{ newsletter.title }} newsletter. {{ newsletter.intro }}
+
+Please click this link to active your subscription:
+
+https://{{ site.domain }}{{ subscription.subscribe_activate_url }}
+
+-Scott
diff --git a/app/lttr/templates/lttr/message/subscribe_subject.txt b/app/lttr/templates/lttr/message/subscribe_subject.txt
new file mode 100644
index 0000000..f4660e0
--- /dev/null
+++ b/app/lttr/templates/lttr/message/subscribe_subject.txt
@@ -0,0 +1 @@
+Confirm Your Subscription to {{newsletter.title}}
diff --git a/app/lttr/templates/lttr/message/unsubscribe.html b/app/lttr/templates/lttr/message/unsubscribe.html
new file mode 100644
index 0000000..4b1a86b
--- /dev/null
+++ b/app/lttr/templates/lttr/message/unsubscribe.html
@@ -0,0 +1,19 @@
+{% load i18n %}<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>{% blocktrans with title=newsletter.title %}Unsubscription from {{ title }}{% endblocktrans %}</title>
+</head>
+<body>
+{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.unsubscribe_activate_url %}Dear {{ name }},
+
+you, or someone in your name requested unsubscription from {{ title }}.
+
+If you would like to confirm your unsubscription, please follow this activation link:
+http://{{ domain }}{{ url }}
+
+Kind regards,{% endblocktrans %}
+{{ newsletter.sender }}
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/message/unsubscribe.txt b/app/lttr/templates/lttr/message/unsubscribe.txt
new file mode 100644
index 0000000..ab31fa5
--- /dev/null
+++ b/app/lttr/templates/lttr/message/unsubscribe.txt
@@ -0,0 +1,9 @@
+{% load i18n %}{% blocktrans with name=subscription.name title=newsletter.title domain=site.domain url=subscription.unsubscribe_activate_url %}Dear {{ name }},
+
+you, or someone in your name requested unsubscription from {{ title }}.
+
+If you would like to confirm your unsubscription, please follow this activation link:
+http://{{ domain }}{{ url }}
+
+Kind regards,{% endblocktrans %}
+{{ newsletter.sender }}
diff --git a/app/lttr/templates/lttr/message/unsubscribe_subject.txt b/app/lttr/templates/lttr/message/unsubscribe_subject.txt
new file mode 100644
index 0000000..49c68ef
--- /dev/null
+++ b/app/lttr/templates/lttr/message/unsubscribe_subject.txt
@@ -0,0 +1 @@
+{% load i18n %}{{ newsletter.title }} - {% trans "Confirm unsubscription" %}
diff --git a/app/lttr/templates/lttr/newslettermailing_detail.html b/app/lttr/templates/lttr/newslettermailing_detail.html
new file mode 100644
index 0000000..1a89200
--- /dev/null
+++ b/app/lttr/templates/lttr/newslettermailing_detail.html
@@ -0,0 +1,155 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'>Luxagraf: thoughts on ecology, culture, travel, photography, walking and other ephemera</title>
+ <link rel="canonical" href="https://luxagraf.net/">{%endblock%}
+
+ {%block extrahead%}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{object.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{{object.meta_description}}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>
+ <meta name="twitter:image:src" content="{{object.get_featured_image}}"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://luxagraf.net{{object.get_absolute_url}}"
+ },
+ "headline": "{{object.title}}",
+ "datePublished": "{{object.pub_date|date:'c'}}+04:00",
+ "dateModified": "{{object.pub_date|date:'c'}}+04:00",
+ "author": {
+ "@type": "Person",
+ "name": "Scott Gilbertson"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "Luxagraf",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://luxagraf.net/media/img/logo-white.jpg"
+ }
+ },
+ "description": "{{object.meta_description}}"
+}
+</script>
+{%endblock%}
+{%block bodyid%}id="home" class="archive"{%endblock%}
+
+{% block primary %}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+ <main>
+ <article class="h-entry hentry entry-content content{% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/BlogPosting">
+ <figure class="large-top-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{%with image=object.featured_image%}
+ <img class="u-photo" itemprop="image" sizes="(max-width: 960px) 100vw"
+ srcset="{{image.get_srcset}}"
+ src="{{image.get_src}}"
+ alt="{{image.alt}} photographed by {% if image.photo_credit_source %}{{image.photo_credit_source}}{%else%}luxagraf{%endif%}">
+ </a>{%endwith%}
+ </figure>
+ <article class="h-entry hentry entry-content content" itemscope itemType="http://schema.org/BlogPosting">
+ <header id="header" class="post-header">
+ <h1 class="p-name post-title" itemprop="headline">{{object.title|smartypants|safe}}</h1>
+ {% if object.subtitle %}<h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>{%endif%}
+ <div class="post-linewrapper">
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content post-body" itemprop="articleBody">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <div class="nav-wrapper">
+ <nav id="page-navigation" {%if wildlife or object.field_notes.all or object.books.all %}{%else%}class="page-border-top"{%endif%}>
+ <ul>{% if prev%}
+ <li id="prev"><span class="bl">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </li>{%endif%}{% if next%}
+ <li id="next"><span class="bl">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ </div>
+ {% if object.related.all %}<div class="article-afterward related">
+ <div class="related-bottom">
+ <h6 class="hedtinycaps">You might also enjoy</h6>
+ <ul class="article-card-list">{% for object in related %}
+ <li class="article-card-mini"><a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ <div class="post-image post-mini-image">
+ {% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %}
+ {% elif object.image %}
+ {% include "lib/img_archive.html" with image=object.image nolightbox=True %}
+ {% else %}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}
+ </div>
+ <h4 class="p-name entry-title post-title" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <p class="post-summary">
+ <span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%}
+ </span>
+ &ndash;
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>
+ </p>
+ </a>
+ </li>
+ {% endfor %}</ul>
+ </div>
+ </div>{%endif%}
+ </main>
+{% endblock %}
+
+{% block js %}{% comment %} <script async src="/media/js/hyphenate.min.js" type="text/javascript"></script>{% endcomment%}{% endblock%}
diff --git a/app/lttr/templates/lttr/postcard_subscribe.html b/app/lttr/templates/lttr/postcard_subscribe.html
new file mode 100644
index 0000000..7f96503
--- /dev/null
+++ b/app/lttr/templates/lttr/postcard_subscribe.html
@@ -0,0 +1,29 @@
+{% load typogrify_tags %}
+<html style="border:none !important;" dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet"
+ href="/media/screenv11.min.css"
+ media="screen">
+<style>
+html { overflow: hidden;}
+.card-subscribe fieldset label {
+ visibility: unset;
+}
+</style>
+</head>
+<body>
+ <form action="" method="post" target='_parent' class="generic-form comment-form card-subscribe">{% csrf_token %}
+ {% for field in form %}
+ <fieldset>
+ {{field.label_tag}}
+ {%ifequal field.name "address"%}<div class="textarea-rounded">{{ field }}</div>{%else%}{{field}}{%endifequal%}
+ </fieldset>
+ {% if forloop.last %}<input type="submit" name="post" class="btn" value="Send Me A Postcard" />{%endif%}
+ <small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small>
+ {%endfor%}
+ </form>
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/postcard_subscribed.html b/app/lttr/templates/lttr/postcard_subscribed.html
new file mode 100644
index 0000000..464fb72
--- /dev/null
+++ b/app/lttr/templates/lttr/postcard_subscribed.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+
+{% block pagetitle %}Thanks for subscribing! | luxagraf.net {% endblock %}
+{% block metadescription %}Thank you, I appreciate you joining the club{% endblock %}
+
+{% block primary %}
+ <nav class="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <span class="nav-item" itemprop="item">
+ <a href="/" itemprop="name">Home</a>
+ <meta itemprop="position" content="1" />
+ </span>
+ <span class="nav-item" itemprop="item">
+ <span itemprop="name">postcards</span>
+ <meta itemprop="position" content="2" />
+ </span>
+ </nav>
+ <main role="main" id="essay-archive" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2>Nicely Done!</h2>
+ <p>We will send you a postcard in the near future.</p>
+ </div>
+ </main>
+{%endblock%}
diff --git a/app/lttr/templates/lttr/range_detail.html b/app/lttr/templates/lttr/range_detail.html
new file mode 100644
index 0000000..008a572
--- /dev/null
+++ b/app/lttr/templates/lttr/range_detail.html
@@ -0,0 +1,184 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load get_image_by_size %}
+{%block htmlclass%}{%endblock%}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'>{{object.title|safe}} by Scott Gilbertson</title>
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}">{%endblock%}
+
+ {%block extrahead%}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{object.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{{object.meta_description}}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>
+ <meta name="twitter:image:src" content="{{object.get_featured_image}}"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://luxagraf.net{{object.get_absolute_url}}"
+ },
+ "headline": "{{object.title}}",
+ "datePublished": "{{object.pub_date|date:'c'}}+04:00",
+ "dateModified": "{{object.pub_date|date:'c'}}+04:00",
+ "author": {
+ "@type": "Person",
+ "name": "Scott Gilbertson"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "Luxagraf",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://luxagraf.net/media/img/logo-white.jpg"
+ }
+ },
+ "description": "{{object.meta_description}}"
+}
+</script>
+{%endblock%}
+{%block bodyid%}id="home" class="friends"{%endblock%}
+{% block breadcrumbs %}<nav class="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <span class="nav-item" itemprop="item">
+ <a href="/" itemprop="name">Home</a>
+ <meta itemprop="position" content="1" />
+ </span>
+ <span class="nav-item" itemprop="item">
+ <a href="/range/" itemprop="name">Range</a>
+ <meta itemprop="position" content="2" />
+ </span>
+ <span class="nav-item" itemprop="item">
+ <span itemprop="name">{{object.get_issue_str}}</span>
+ <meta itemprop="position" content="3" />
+ </span>
+ </nav>
+{% endblock %}
+{% block primary %}
+ <main>
+ <figure class="large-top-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{%with image=object.featured_image%}
+ <img style="margin:0;" class="u-photo" itemprop="image" sizes="(max-width: 960px) 100vw"
+ srcset="{{image.get_srcset}}"
+ src="{{image.get_src}}"
+ alt="{{image.alt}} photographed by {% if image.photo_credit_source %}{{image.photo_credit_source}}{%else%}luxagraf{%endif%}">
+ </a>
+ <figcaption class="exif-caption">
+ {{image.exif_make}} {{image.exif_model}} {%if image.exif_lens %} with a {{image.exif_lens}} lens, {%endif%} f/{{image.exif_aperture}} for {{image.exif_exposure}} sec at {{image.exif_iso}} ISO.
+ </figcaption>
+ </figure>{%endwith%}
+ <article class="h-entry hentry content" itemscope itemType="http://schema.org/BlogPosting">
+ <header id="header" class="post-header">
+ <h1 class="p-name post-title" itemprop="headline">{{object.title|smartypants|safe}}</h1>
+ <div class="post-dateline">
+ <time class="dt-published published dt-updated post-date lttr-box" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">Image {{object.get_issue_str}} &ndash; {{object.pub_date|date:"F j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post-body" itemprop="articleBody">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {%if object.books.all %}<div class="entry-footer">
+ <aside id="recommended-reading" class="" >
+ <h3>Recommended Reading</h3>{% for obj in object.books.all %}
+ <div itemprop="mainEntity" itemscope itemtype="http://schema.org/Book">
+ <div class="book-cover-wrapper">
+ <img src="{{obj.get_image_url}}" alt="{{obj.title}} cover" class="lttr-cover" />
+ </div>
+ <div class="meta-cover">
+ <h5 class="post-title book-title" itemprop="name">{{obj.title|smartypants|widont|safe}}</h6>
+ <h6 class="post-subtitle" itemprop="author" itemscope itemtype="http://schema.org/Person">
+ <meta itemprop="name" content="{{obj.author_name}}"/>by {{obj.author_name}}</h5>
+ <dl class="book-metadata">
+ {% if obj.rating %}<dt>Rating</dt><dd class="book-stars">
+ {% for i in obj.ratings_range %}{% if i <= obj.get_rating%}&#9733;{%else%}&#9734;{%endif%}{%endfor%}</span></dd>{%endif%}
+ {% if obj.read_in %}<dt>Read</dt>
+ <dd>{{obj.read_in}}</dd>{%endif%}
+ {% if obj.pages %}<dt>Pages</dt>
+ <dd itemprop="numberOfPages">{{obj.pages}}</dd>{%endif%}
+ {% if obj.publish_date %}<dt>Published</dt>
+ <dd>{%if obj.publish_place%}{{obj.publish_place}}, {%endif%}{{obj.publish_date}}</dd>{%endif%}
+ {% if obj.isbn %}<dt>ISBN</dt>
+ <dd>{{obj.isbn}}</dd>{%endif%}
+ </dl>
+ <div class="buy-btn-wrapper">
+ {% if obj.isbn %}<a class="buy-btn" href="http://worldcat.org/isbn/{{obj.isbn}}" title="find {{obj.title}} in your local library">Borrow</a>{%endif%}
+ {% if obj.afflink %}<a class="buy-btn" href="{{obj.afflink}}" title="buy {{obj.title}} at Amazon">Buy</a>{%endif%}
+ </div>
+ </div>{%if obj.body_html%}
+ <div class="thoughts" itemprop="review" itemscope itemtype="http://schema.org/Review">
+ <h5>Notes</h5>
+ <span class="hide" itemprop="reviewRating">{{obj.rating}}</span>
+ <meta itemprop="author" content="Scott Gilbertson" />
+ <meta itemprop="datePublished" content="{{obj.read_date|date:"c"}}">
+ <div itemprop="reviewBody">{{obj.body_html|safe|smartypants|widont}}</div>
+ </div>{%endif%}
+ </div>
+ {% endfor %}
+ </aside>{%endif%}
+ </article>
+
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <nav class="page-navigation">
+ <div>{% if prev%}
+ <span class="label">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </div>{%endif%}{% if next %}
+ <div>
+ <span class="label">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </div>{%endif%}
+ </nav>{%endwith%}{%endwith%}
+ <aside class="narrow donate join">
+ <p>You're reading <em>Range</em>, a weekly mailing of a single photograph, along with a few notes, and video of the processing. If you'd like to join us, drop your email in the form below: </p>
+ <iframe target='_parent' style="border:none !important; background:white; width:100% !important;" title="embedded form for subscribing the the Friends of a Long Year newsletter" src="{% url 'lttr:subscribe' slug='range' %}"></iframe>
+ </aside>
+ </div>
+ {% if object.related.all %}<div class="article-afterward related">
+ <div class="related-bottom">
+ <h6 class="hedtinycaps">You might also enjoy</h6>
+ <ul class="article-card-list">{% for object in related %}
+ <li class="article-card-mini"><a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ <div class="post-image post-mini-image">
+ {% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %}
+ {% elif object.image %}
+ {% include "lib/img_archive.html" with image=object.image nolightbox=True %}
+ {% else %}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}
+ </div>
+ <h4 class="p-name entry-title post-title" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <p class="post-summary">
+ <span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%}
+ </span>
+ &ndash;
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>
+ </p>
+ </a>
+ </li>
+ {% endfor %}</ul>
+ </div>
+ </div>{%endif%}
+ </main>
+{% endblock %}
+
+{% block js %}{% comment %} <script async src="/media/js/hyphenate.min.js" type="text/javascript"></script>{% endcomment%}{% endblock%}
+
+
diff --git a/app/lttr/templates/lttr/range_list.html b/app/lttr/templates/lttr/range_list.html
new file mode 100644
index 0000000..c6875e9
--- /dev/null
+++ b/app/lttr/templates/lttr/range_list.html
@@ -0,0 +1,44 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block pagetitle %}Luxagraf | Range {% endblock %}
+{% block metadescription %}A weekly photo, developed.{% endblock %}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+<main role="main" class="archive-wrapper">{% for object in object_list %}{% if forloop.first %}
+ <figure class="large-top-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{%with image=object.featured_image%}
+ <img style="margin:0;" class="u-photo" itemprop="image" sizes="(max-width: 960px) 100vw"
+ srcset="{{image.get_srcset}}"
+ src="{{image.get_src}}"
+ alt="{{image.alt}} photographed by {% if image.photo_credit_source %}{{image.photo_credit_source}}{%else%}luxagraf{%endif%}">
+ </a>{%endwith%}{%endif%}{%endfor%}
+ </figure>
+ <div class="archive-intro">
+ <h1 class="list-hed">Range</h1>
+ <h2 class="list-subhed">A weekly photo, developed.</h2>
+ <p>Please join us by dropping your email in the form below: </p>
+ <iframe target='_parent' style="border:none !important; background:white; width:100% !important;" title="embedded form for subscribing the the Friends of a Long Year newsletter" src="{% url 'lttr:subscribe' slug='range' %}"></iframe>
+ <p><em>Range</em> is a weekly mailing of a single photograph. </p>
+ <p>If you're interested there is also a link to a video of the RAW image processing in <a href="https://www.darktable.org/">darktable</a>, and sometimes a few words about the process. But the primary purpose is to deliver a single photo to your inbox. Simple and fun.</p>
+ <p>Yes, I know about Instagram. This is an attempt to reclaim that space, sharing photos with friends, but without all the distractions of the corporate social web, without the endless scroll of photos, likes, stories, comments, whatever. This is just an image delivered once a week to your inbox. I've been trying to think of a way to make it reciprocal, so you can send a picture to my inbox. If you have ideas, <a href="mailto:comments@luxagraf.net">email me</a>.</p>
+ <p>Unsubscribing is easy. It's <a href="/src/building-your-own-mailing-list-software">self-hosted</a> and <a href="/privacy" title="My privacy policy">respects your privacy</a>. If you don't want an email, there's also <a href="/range/feed.xml">an RSS feed</a>, and it's all archived below.</p>
+ <p>There's also the <em><a href="/friends/">Friends of a Long Year</a></em> newsletter if you want some stories in your inbox.</p>
+ </div>
+ <h3 class="archive-sans">Images</h3>
+ <div class="archive-grid">{% for object in object_list %}
+ <article class="h-entry hentry archive-grid-card" itemscope itemType="http://schema.org/Article">
+ <div class="card-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ {% include "lib/img_archive.html" with image=object.featured_image %}
+ </a>
+ </div>
+ <h2 class="p-name card-hed-it" itemprop="headline"><a href="{{object.get_absolute_url}}" class="u-url" title="{{object.title}}">{{object.title|safe|smartypants|widont}}</a></h2>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <time class="dt-published published dt-updated card-smcaps" datetime="{{object.pub_date|date:'c'}}">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ </article> {% endfor %}
+ </div>
+ </main>
+{%endblock%}
+
+ <p>If you're not familiar, darktable is open source raw image developer. It's free, you can download a copy for Linux, macOS, or Windows. The <a href="https://www.darktable.org/usermanual/en/">darktable user manual</a> is very helpful if you're brand new. I also recommend <a href="https://www.youtube.com/user/audio2u">Bruce Williams' darktable videos</a>, and <a href="https://www.youtube.com/user/s7habo/videos">Boris Hajdukovic's videos</a>, which were the inspiration for what you see here.</p>
+ <p>I'm no expert either, so feel free to hit reply and let me know if I get something wrong.</p>
diff --git a/app/lttr/templates/lttr/range_subscribe.html b/app/lttr/templates/lttr/range_subscribe.html
new file mode 100644
index 0000000..e73ca73
--- /dev/null
+++ b/app/lttr/templates/lttr/range_subscribe.html
@@ -0,0 +1,23 @@
+{% load typogrify_tags %}
+<html style="border:none !important;" dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet"
+ href="/media/screenv10.css"
+ media="screen">
+</head>
+<body>
+ <form action="" method="post" target='_parent' class="generic-form flex newsletter-subscribe">{% csrf_token %}
+ {% for field in form %}
+ <fieldset>
+ {{field.label_tag}}
+ {{field}}
+ </fieldset>
+ {% if forloop.last %}<input type="submit" name="post" class="btn" value="Subscribe" />{%endif%}
+ </form>
+ <small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small>
+ {%endfor%}
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/subscribe.html b/app/lttr/templates/lttr/subscribe.html
new file mode 100644
index 0000000..e73ca73
--- /dev/null
+++ b/app/lttr/templates/lttr/subscribe.html
@@ -0,0 +1,23 @@
+{% load typogrify_tags %}
+<html style="border:none !important;" dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet"
+ href="/media/screenv10.css"
+ media="screen">
+</head>
+<body>
+ <form action="" method="post" target='_parent' class="generic-form flex newsletter-subscribe">{% csrf_token %}
+ {% for field in form %}
+ <fieldset>
+ {{field.label_tag}}
+ {{field}}
+ </fieldset>
+ {% if forloop.last %}<input type="submit" name="post" class="btn" value="Subscribe" />{%endif%}
+ </form>
+ <small class="alert">{% if field.errors %}{{field.errors}}{% endif %}</small>
+ {%endfor%}
+</body>
+</html>
diff --git a/app/lttr/templates/lttr/subscribed.html b/app/lttr/templates/lttr/subscribed.html
new file mode 100644
index 0000000..43278cf
--- /dev/null
+++ b/app/lttr/templates/lttr/subscribed.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+
+{% block pagetitle %}Thanks for subscribing! | luxagraf.net {% endblock %}
+{% block metadescription %}Thank you, I appreciate you joining the club{% endblock %}
+
+{% block primary %}
+ <nav class="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <span class="nav-item" itemprop="item">
+ <a href="/" itemprop="name">Home</a>
+ <meta itemprop="position" content="1" />
+ </span>
+ <span class="nav-item" itemprop="item">
+ <span itemprop="name">lttr</span>
+ <meta itemprop="position" content="2" />
+ </span>
+ </nav>
+ <main role="main" id="essay-archive" class="archive-wrapper">
+ <div class="archive-intro">
+ <h2>Thanks, You're Almost There.</h2>
+ <p><b>Check your email for a link to confirm your subscription</b></p>
+ </div>
+ </main>
+{%endblock%}
diff --git a/app/lttr/templates/lttr/unsubscribe.html b/app/lttr/templates/lttr/unsubscribe.html
new file mode 100644
index 0000000..993ea9f
--- /dev/null
+++ b/app/lttr/templates/lttr/unsubscribe.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+
+{% block pagetitle %}Luxagraf | Friends of a Long Year {% endblock %}
+{% block metadescription %}An infrequesnt mailing list about travel, photography, tools, walking, the natural world and other ephemera.{% endblock %}
+
+{% block primary %}
+<nav class="breadcrumbs" itemscope="" itemtype="http://schema.org/BreadcrumbList">
+ <span class="nav-item" itemprop="item">
+ <a href="/" itemprop="name">Home</a>
+ <meta itemprop="position" content="1">
+ </span>
+ <span class="nav-item" itemprop="item">
+ <span itemprop="name">Unsubscribe</span>
+ <meta itemprop="position" content="2">
+ </span>
+</nav>
+ <main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>You're unsubscribed, so long friend</h2>
+ <p>If you clicked by mistake you can always <a href="{% url 'lttr:newsletter_activate' slug=newsletter activation_code=subscriber.activation_code %}">rejoin our merry band</a>.</p>
+ </div>
+ </main>
+{%endblock%}
diff --git a/app/lttr/urls.py b/app/lttr/urls.py
new file mode 100644
index 0000000..abeac11
--- /dev/null
+++ b/app/lttr/urls.py
@@ -0,0 +1,27 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "lttr"
+
+urlpatterns = [
+ path(
+ '<str:slug>/unsubscribe/<str:activation_code>',
+ views.UnsubscribeRequestView.as_view(),
+ name='newsletter_unsubscribe'
+ ),
+ path(
+ r'<str:slug>/subscribe/',
+ views.NewsletterSubscribeView.as_view(),
+ name="subscribe"
+ ),
+ path(
+ '<str:slug>/activate/<str:activation_code>/',
+ views.ConfirmSubscriptionView.as_view(), name='newsletter_activate'
+ ),
+ path(
+ r'subscribed/',
+ views.NewsletterSubscribedView.as_view(),
+ name="subscribed"
+ ),
+]
diff --git a/app/lttr/validators.py b/app/lttr/validators.py
new file mode 100644
index 0000000..a6355bf
--- /dev/null
+++ b/app/lttr/validators.py
@@ -0,0 +1,19 @@
+from django.contrib.auth import get_user_model
+from django.forms.utils import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+
+def validate_email_nouser(email):
+ """
+ Check if the email address does not belong to an existing user.
+ """
+ # Check whether we should be subscribed to as a user
+ #User = get_user_model()
+
+ #if User.objects.filter(email__exact=email).exists():
+ # raise ValidationError(_(
+ # "The e-mail address '%(email)s' belongs to a user with an "
+ # "account on this site. Please log in as that user "
+ # "and try again."
+ # ) % {'email': email})
+ pass
diff --git a/app/lttr/views.py b/app/lttr/views.py
new file mode 100644
index 0000000..d17a609
--- /dev/null
+++ b/app/lttr/views.py
@@ -0,0 +1,108 @@
+import socket
+from django.views.generic import ListView, CreateView, TemplateView, FormView
+from django.views.generic.detail import DetailView
+from django.views.generic.dates import YearArchiveView, MonthArchiveView
+from django.contrib.syndication.views import Feed
+from django.template.response import TemplateResponse
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+from django.shortcuts import get_object_or_404, redirect
+from django.conf import settings
+from django.urls import reverse, reverse_lazy
+
+from utils.views import PaginatedListView, LuxDetailView
+
+from smtplib import SMTPException
+from .models import Subscriber, Newsletter
+from .forms import SubscribeRequestForm, UpdateForm
+
+ACTIONS = ('subscribe', 'unsubscribe', 'update')
+
+
+class NewsletterSubscribedView(TemplateView):
+ template_name = "lttr/subscribed.html"
+
+
+class NewsletterOptionsView(ListView):
+ model = Newsletter
+
+
+class ConfirmSubscriptionView(DetailView):
+ model = Subscriber
+ template_name = "lttr/confirm_activate.html"
+
+ def get_object(self):
+ obj = Subscriber.objects.get(newsletter__slug=self.kwargs['slug'], activation_code=self.kwargs['activation_code'])
+ if obj.subscribed is False:
+ obj.update('subscribe')
+ return obj
+
+ def get_context_data(self, **kwargs):
+ context = super(ConfirmSubscriptionView, self).get_context_data(**kwargs)
+ context['newsletter'] = self.kwargs['slug']
+ return context
+
+
+class NewsletterSubscribeView(CreateView):
+ """
+ Return a subscribe form for iframe embedding
+ """
+ model = Subscriber
+ form_class = SubscribeRequestForm
+ action = 'subscribe'
+ slug = None
+
+
+ def get_template_names(self):
+ return ["lttr/%s_subscribe.html" % self.slug, 'lttr/subscribe.html']
+
+ def get_form_kwargs(self):
+ kwargs = super(NewsletterSubscribeView, self).get_form_kwargs()
+ try:
+ self.slug = self.kwargs['slug']
+ except:
+ pass
+ nl = Newsletter.objects.get(slug=self.slug)
+ kwargs['newsletter'] = nl
+ return kwargs
+
+ def get_success_url(self):
+ return reverse_lazy('lttr:subscribed')
+
+ def form_valid(self, form, **kwargs):
+ form.instance.user, created = get_user_model().objects.get_or_create(
+ email=form.cleaned_data['email_field'],
+ username=form.cleaned_data['email_field']
+ )
+ self.object = form.save()
+ try:
+ self.object.send_activation_email(action=self.action)
+
+ except (SMTPException, socket.error) as e:
+ print(e)
+ self.error = True
+
+ # Although form was valid there was error while sending email,
+ # so stay at the same url.
+ return super(NewsletterSubscribeView, self).form_invalid(form)
+ return super(NewsletterSubscribeView, self).form_valid(form)
+
+
+class UnsubscribeRequestView(DetailView):
+ model = Subscriber
+ template_name = "lttr/unsubscribe.html"
+
+ def get_object(self):
+ obj = Subscriber.objects.get(newsletter__slug=self.kwargs['slug'],activation_code=self.kwargs['activation_code'])
+ if obj.subscribed is True:
+ obj.update('unsubscribe')
+ return obj
+
+
+ def get_context_data(self, **kwargs):
+ context = super(UnsubscribeRequestView, self).get_context_data(**kwargs)
+ context['subscriber'] = self.get_object()
+ context['newsletter'] = self.kwargs['slug']
+ return context
+
+
diff --git a/app/media/__init__.py b/app/media/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/media/__init__.py
diff --git a/app/media/admin.py b/app/media/admin.py
new file mode 100644
index 0000000..12d0509
--- /dev/null
+++ b/app/media/admin.py
@@ -0,0 +1,37 @@
+from django.contrib import admin
+from django.contrib.gis.admin import OSMGeoAdmin
+
+from .models import LuxImage, LuxGallery, LuxImageSize, LuxVideo, LuxAudio
+
+
+@admin.register(LuxImageSize)
+class LuxImageSizeAdmin(OSMGeoAdmin):
+ list_display = ('name', 'width', 'height', 'quality')
+
+
+@admin.register(LuxVideo)
+class LuxVideoAdmin(OSMGeoAdmin):
+ pass
+
+
+@admin.register(LuxImage)
+class LuxImageAdmin(OSMGeoAdmin):
+ list_display = ('pk', 'admin_thumbnail', 'pub_date', 'caption')
+ list_filter = ('pub_date',)
+ search_fields = ['title', 'caption']
+ # Options for OSM map Using custom ESRI topo map
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', ('image'), 'pub_date', 'sizes', 'alt', 'caption', ('is_public'), ('photo_credit_source', 'photo_credit_url'))
+ }),
+ )
+
+ class Media:
+ js = ('image-preview.js', 'next-prev-links.js')
+
+
+@admin.register(LuxAudio)
+class LuxAudioAdmin(OSMGeoAdmin):
+ list_display = ('pk', 'title', 'pub_date')
+ list_filter = ('pub_date',)
diff --git a/app/media/build.py b/app/media/build.py
new file mode 100644
index 0000000..e95cbfc
--- /dev/null
+++ b/app/media/build.py
@@ -0,0 +1,48 @@
+import os
+from django.urls import reverse
+from builder.base import BuildNew
+
+from .models import LuxImage
+
+
+class BuildLuxPhotos(BuildNew):
+
+ def build(self):
+ self.build_detail_view()
+ self.build_daily_photo()
+
+ def get_model_queryset(self):
+ return self.model.objects.all()
+
+ def build_daily_photo(self):
+ '''
+ build out images that I post daily, found by title prefix daily_
+ '''
+ self.build_list_view(
+ base_path=reverse("photos:daily_photo_list"),
+ qs=LuxImage.objects.filter(is_public=True, title__startswith="daily_"),
+ paginate_by=10
+ )
+
+ def build_detail_view(self):
+ '''
+ write out all the expenses for each trip
+ '''
+ for obj in self.get_model_queryset():
+ url = obj.get_absolute_url()
+ path, slug = os.path.split(url)
+ path = '%s/' % path
+ # write html
+ response = self.client.get(url)
+ print(path, slug)
+ self.write_file(path, response.content, filename=slug)
+
+
+def dailybuilder():
+ j = BuildLuxPhotos("photos", "LuxImage")
+ j.build_daily_photo()
+
+
+def builder():
+ j = BuildLuxPhotos("photos", "LuxGallery")
+ j.build()
diff --git a/app/media/migrations/0001_initial.py b/app/media/migrations/0001_initial.py
new file mode 100644
index 0000000..623e685
--- /dev/null
+++ b/app/media/migrations/0001_initial.py
@@ -0,0 +1,111 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:30
+
+import datetime
+from django.db import migrations, models
+import django.db.models.deletion
+import media.models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LuxAudio',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('subtitle', models.CharField(blank=True, max_length=200)),
+ ('slug', models.SlugField(blank=True, unique_for_date='pub_date')),
+ ('body_html', models.TextField(blank=True)),
+ ('body_markdown', models.TextField(blank=True)),
+ ('pub_date', models.DateTimeField(default=datetime.datetime.now)),
+ ('mp3', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)),
+ ('ogg', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)),
+ ],
+ options={
+ 'verbose_name': 'Audio',
+ 'verbose_name_plural': 'Audio',
+ 'ordering': ('-pub_date',),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxImageSize',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, max_length=30, null=True)),
+ ('width', models.IntegerField(blank=True, null=True)),
+ ('height', models.IntegerField(blank=True, null=True)),
+ ('quality', models.IntegerField()),
+ ],
+ options={
+ 'verbose_name_plural': 'Image Sizes',
+ 'ordering': ('-name', 'id'),
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxVideo',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('video_mp4', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)),
+ ('video_webm', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)),
+ ('video_poster', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)),
+ ('title', models.CharField(blank=True, max_length=300, null=True)),
+ ('pub_date', models.DateTimeField(default=datetime.datetime.now)),
+ ('youtube_url', models.CharField(blank=True, max_length=80, null=True)),
+ ('vimeo_url', models.CharField(blank=True, max_length=300, null=True)),
+ ],
+ options={
+ 'verbose_name': 'Video',
+ 'verbose_name_plural': 'Videos',
+ 'ordering': ('-pub_date', 'id'),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxImage',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('image', models.FileField(blank=True, null=True, upload_to=media.models.get_upload_path)),
+ ('title', models.CharField(blank=True, max_length=300, null=True)),
+ ('alt', models.CharField(blank=True, max_length=300, null=True)),
+ ('photo_credit_source', models.CharField(blank=True, max_length=300, null=True)),
+ ('photo_credit_url', models.CharField(blank=True, max_length=300, null=True)),
+ ('caption', models.TextField(blank=True, null=True)),
+ ('pub_date', models.DateTimeField(default=datetime.datetime.now)),
+ ('height', models.CharField(blank=True, max_length=6, null=True)),
+ ('width', models.CharField(blank=True, max_length=6, null=True)),
+ ('is_public', models.BooleanField(default=True)),
+ ('sizes', models.ManyToManyField(blank=True, to='media.LuxImageSize')),
+ ],
+ options={
+ 'verbose_name_plural': 'Images',
+ 'ordering': ('-pub_date', 'id'),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxGallery',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(blank=True, max_length=300)),
+ ('description', models.TextField(blank=True, null=True)),
+ ('slug', models.CharField(blank=True, max_length=300)),
+ ('pub_date', models.DateTimeField(null=True)),
+ ('is_public', models.BooleanField(default=True)),
+ ('caption_style', models.CharField(blank=True, max_length=400, null=True)),
+ ('images', models.ManyToManyField(to='media.LuxImage')),
+ ('thumb', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gallery_thumb', to='media.luximage')),
+ ],
+ options={
+ 'verbose_name_plural': 'Galleries',
+ 'ordering': ('-pub_date', 'id'),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ ]
diff --git a/app/media/migrations/__init__.py b/app/media/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/media/migrations/__init__.py
diff --git a/app/media/models.py b/app/media/models.py
new file mode 100644
index 0000000..c385018
--- /dev/null
+++ b/app/media/models.py
@@ -0,0 +1,359 @@
+import os.path
+import io
+import datetime
+from PIL import Image
+
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.contrib.sitemaps import Sitemap
+from django.utils.encoding import force_text
+from django.utils.functional import cached_property
+from django.urls import reverse
+from django.apps import apps
+from django.utils.html import format_html
+from django.utils.text import slugify
+from django.conf import settings
+from django import forms
+
+from taggit.managers import TaggableManager
+
+from resizeimage.imageexceptions import ImageSizeError
+
+from .utils import resize_image
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.db.models.signals import m2m_changed
+
+
+def get_upload_path(self, filename):
+ return "images/original/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+def get_vid_upload_path(self, filename):
+ return "images/videos/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+def get_audio_upload_path(self, filename):
+ return "audio/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+class LuxImageSize(models.Model):
+ name = models.CharField(null=True, blank=True, max_length=30)
+ width = models.IntegerField(null=True, blank=True)
+ height = models.IntegerField(null=True, blank=True)
+ quality = models.IntegerField()
+
+ class Meta:
+ ordering = ('-name', 'id')
+ verbose_name_plural = 'Image Sizes'
+
+ def __str__(self):
+ if self.width:
+ size = self.width
+ if self.height:
+ size = self.height
+ return "%s - %s" %(self.name, str(size))
+
+
+class LuxImage(models.Model):
+ image = models.FileField(blank=True, null=True, upload_to=get_upload_path)
+ title = models.CharField(null=True, blank=True, max_length=300)
+ alt = models.CharField(null=True, blank=True, max_length=300)
+ photo_credit_source = models.CharField(null=True, blank=True, max_length=300)
+ photo_credit_url = models.CharField(null=True, blank=True, max_length=300)
+ caption = models.TextField(blank=True, null=True)
+ pub_date = models.DateTimeField(default=datetime.datetime.now)
+ height = models.CharField(max_length=6, blank=True, null=True)
+ width = models.CharField(max_length=6, blank=True, null=True)
+ is_public = models.BooleanField(default=True)
+ sizes = models.ManyToManyField(LuxImageSize, blank=True)
+
+ class Meta:
+ ordering = ('-pub_date', 'id')
+ verbose_name_plural = 'Images'
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ if self.title:
+ return "%s" % self.title
+ else:
+ return "%s" % self.pk
+
+ def get_type(self):
+ return str(self.__class__.__name__)
+
+ def get_admin_image(self):
+ for size in self.sizes.all():
+ if size.width and size.width <= 820 or size.height and size.height <= 800:
+ return self.get_image_by_size(size.name)
+
+ def get_admin_insert(self):
+ return "/media/images/%s/%s_tn.%s" % (self.pub_date.strftime("%Y"), self.get_image_name(), self.get_image_ext())
+
+ def get_largest_image(self):
+ t = []
+ for size in self.sizes.all():
+ t.append(size.width)
+ t.sort(key=float)
+ t.reverse()
+ return self.get_image_path_by_size(t[0])
+
+ def get_image_name(self):
+ return self.image.url.split("original/")[1][5:-4]
+
+ def get_image_ext(self):
+ return self.image.url[-3:]
+
+ @cached_property
+ def get_featured_jrnl(self):
+ ''' cached version of getting the primary image for archive page'''
+ return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'featured_jrnl', self.get_image_ext())
+
+ @cached_property
+ def get_picwide_sm(self):
+ ''' cached version of getting the second image for archive page'''
+ return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'picwide-sm', self.get_image_ext())
+
+ @cached_property
+ def get_srcset(self):
+ srcset = ""
+ length = len(self.sizes.all())
+ print(length)
+ loopnum = 1
+ for size in self.sizes.all():
+ srcset += "%s%s/%s_%s.%s %sw" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), size.name, self.get_image_ext(), size.width)
+ if loopnum < length:
+ srcset += ", "
+ loopnum = loopnum+1
+ return srcset
+
+ @cached_property
+ def get_src(self):
+ src = ""
+ if self.sizes.all().count() > 1:
+ src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), 'picwide-med', self.get_image_ext())
+ else:
+ src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.get_image_name(), [size.name for size in self.sizes.all()], self.get_image_ext())
+ return src
+
+ def get_image_by_size(self, size="original"):
+ base = self.get_image_name()
+ if size == "admin_insert":
+ return "images/%s/%s.%s" % (self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ if size == "original":
+ return "%soriginal/%s/%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ else:
+ if size != 'tn':
+ s = LuxImageSize.objects.get(name=size)
+ if s not in self.sizes.all():
+ print("new size is "+s.name)
+ self.sizes.add(s)
+ return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, size, self.get_image_ext())
+
+ def get_image_path_by_size(self, size="original"):
+ base = self.get_image_name()
+ if size == "original":
+ return "%s/original/%s/%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ else:
+ return "%s/%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, size, self.get_image_ext())
+
+ def get_thumbnail_url(self):
+ return self.get_image_by_size("tn")
+
+ def admin_thumbnail(self):
+ return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_by_size(), self.get_image_by_size("tn")))
+ admin_thumbnail.short_description = 'Thumbnail'
+
+ def get_sizes(self):
+ return self.sizes.all()
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date()
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date()
+
+ @property
+ def get_previous_admin_url(self):
+ n = self.get_previous_by_pub_date()
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[n.id] )
+
+ @property
+ def get_next_admin_url(self):
+ model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name)
+ try:
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk] )
+ except model.DoesNotExist:
+ return ''
+
+ @property
+ def is_portait(self):
+ if int(self.height) > int(self.width):
+ return True
+ else:
+ return False
+
+ def save(self, *args, **kwargs):
+ super(LuxImage, self).save()
+
+
+class LuxGallery(models.Model):
+ title = models.CharField(blank=True, max_length=300)
+ description = models.TextField(blank=True, null=True)
+ slug = models.CharField(blank=True, max_length=300)
+ thumb = models.ForeignKey(LuxImage, on_delete=models.CASCADE, related_name="gallery_thumb", null=True, blank=True)
+ images = models.ManyToManyField(LuxImage)
+ pub_date = models.DateTimeField(null=True)
+ is_public = models.BooleanField(default=True)
+ caption_style = models.CharField(blank=True, null=True, max_length=400)
+
+ class Meta:
+ ordering = ('-pub_date', 'id')
+ verbose_name_plural = 'Galleries'
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ return self.title
+
+ def get_main_image(self):
+ return "%sgallery_thumbs/%s.jpg" % (settings.IMAGES_URL, self.id)
+
+ def get_absolute_url(self):
+ if self.is_public:
+ return "/photos/galleries/%s" % (self.slug)
+ else:
+ return "/photos/galleries/private/%s" % (self.slug)
+
+ def thumbs(self):
+ lst = [x.image.name for x in self.images.all()]
+ lst = ["<a href='/media/%s'>%s</a>" % (x, x.split('/')[-1]) for x in lst]
+ return ', '.join(item for item in lst if item)
+ thumbs.allow_tags = True
+
+
+class LuxVideo(models.Model):
+ video_mp4 = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ video_webm = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ video_poster = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ title = models.CharField(null=True, blank=True, max_length=300)
+ pub_date = models.DateTimeField(default=datetime.datetime.now)
+ youtube_url = models.CharField(null=True, blank=True, max_length=80)
+ vimeo_url = models.CharField(null=True, blank=True, max_length=300)
+
+ def __str__(self):
+ if self.title:
+ return self.title
+ else:
+ return str(self.pk)
+
+ def get_type(self):
+ return str(self.__class__.__name__)
+
+ class Meta:
+ ordering = ('-pub_date', 'id')
+ verbose_name_plural = 'Videos'
+ verbose_name = 'Video'
+ get_latest_by = 'pub_date'
+
+ def save(self, *args, **kwargs):
+ super(LuxVideo, self).save(*args, **kwargs)
+
+
+class LuxAudio(models.Model):
+ title = models.CharField(max_length=200)
+ subtitle = models.CharField(max_length=200, blank=True)
+ slug = models.SlugField(unique_for_date='pub_date', blank=True)
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField(blank=True)
+ pub_date = models.DateTimeField(default=datetime.datetime.now)
+ mp3 = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path)
+ ogg = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path)
+
+ class Meta:
+ ordering = ('-pub_date',)
+ verbose_name_plural = 'Audio'
+ verbose_name = 'Audio'
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("prompt:detail", kwargs={"slug": self.slug})
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date(status__exact=1)
+
+ @property
+ def get_previous_admin_url(self):
+ n = self.get_previous_by_pub_date()
+ return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[n.id])
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date(status__exact=1)
+
+ @property
+ def get_next_admin_url(self):
+ model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name)
+ try:
+ return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk])
+ except model.DoesNotExist:
+ return ''
+
+ def get_type(self):
+ return str(self.__class__.__name__)
+
+ def save(self, *args, **kwargs):
+ md = render_images(self.body_markdown)
+ self.body_html = markdown_to_html(md)
+ super(LuxAudio, self).save(*args, **kwargs)
+
+
+@receiver(post_save, sender=LuxImage)
+def post_save_events(sender, update_fields, created, instance, **kwargs):
+ filename, file_extension = os.path.splitext(instance.image.path)
+ if file_extension != ".mp4":
+ img = Image.open(instance.image.path)
+ instance.height = img.height
+ instance.width = img.width
+ post_save.disconnect(post_save_events, sender=LuxImage)
+ instance.save()
+ post_save.connect(post_save_events, sender=LuxImage)
+
+
+@receiver(m2m_changed, sender=LuxImage.sizes.through)
+def update_photo_sizes(sender, instance, **kwargs):
+ base_path = "%s/%s/" % (settings.IMAGES_ROOT, instance.pub_date.strftime("%Y"))
+ filename, file_extension = os.path.splitext(instance.image.path)
+ if file_extension != ".mp4":
+ img = Image.open(instance.image.path)
+ resize_image(img, 160, None, 78, base_path, "%s_tn.%s" % (instance.get_image_name(), instance.get_image_ext()))
+ for size in instance.sizes.all():
+ if size.width:
+ print("Image width is:"+str(img.width))
+ try:
+ if size.width <= img.width:
+ resize_image(img, size.width, None, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext()))
+ else:
+ raise ValidationError({'items': ["Size is larger than source image"]})
+ except ImageSizeError:
+ m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through)
+ instance.sizes.remove(size)
+ m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through)
+ if size.height:
+ try:
+ if size.height <= img.height:
+ resize_image(img, None, size.height, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext()))
+
+ else:
+ pass
+ except ImageSizeError:
+ m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through)
+ instance.sizes.remove(size)
+ m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through)
+
+
diff --git a/app/media/photos.js b/app/media/photos.js
new file mode 100644
index 0000000..b93467a
--- /dev/null
+++ b/app/media/photos.js
@@ -0,0 +1,71 @@
+//Utility functions for map info window
+function mapit(obj) {
+ lat = parseFloat(obj.attr('data-latitude'));
+ lon = parseFloat(obj.attr('data-longitude'));
+ elid= obj.attr('data-imgid');
+ map = L.map(document.getElementById("mw-"+elid));
+ centerCoord = new L.LatLng(lat, lon);
+ zoom = 8;
+ L.tileLayer.provider('Esri.WorldTopoMap', {maxZoom: 18, attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Tiles &copy; Esri and the GIS User Community'}).addTo(map);
+ map.setView(centerCoord, zoom);
+ L.marker([lat, lon]).addTo(map);
+}
+ //########## utility functions to create/remove map container ############
+function create_map(obj) {
+ //find id of this image caption:
+ var imgid = obj.attr('data-imgid');
+ //create container divs
+ $('<div class="map-container" id="mc-'+imgid+'">').insertBefore($(obj).parent().parent());
+ //$(obj).parent().parent().parent().prepend('<div class="map-container" id="mc-'+imgid+'">');
+ $('#mc-'+imgid).append('<div class="map-wrapper" id="mw-'+imgid+'">');
+ //deal with the variable height of div.legend
+ $('#mc-'+imgid).css({
+ bottom: function(index, value) {
+ return parseFloat($(obj).parent().parent().height())+20;
+ }
+ });
+
+ mapit(obj);
+}
+function remove_map(imgid) {
+ $('#mc-'+imgid).remove();
+}
+
+//############ Document.ready events ##############
+$(document).ready(function(){
+
+ //set up click events for map button
+ $('.map-link').click( function() {
+ imgid = $(this).attr('data-imgid');
+ if ($('#mc-'+imgid).is(":visible")) {
+ remove_map(imgid);
+ } else {
+ create_map($(this));
+ }
+ return false;
+
+ });
+ var $ele = $('#slides').children();
+ var $curr = 0;
+ $(document).bind('keydown', function (e) {
+ var code = e.which;
+ switch (code) {
+ case 39:
+ if ($curr <= $ele.size()) {
+ $.scrollTo($ele[$curr], 800 );
+ $curr++;
+ }
+ break;
+ case 37:
+ if ($curr > 0) {
+ $curr--;
+ var $now = $curr;
+ $now--;
+ $.scrollTo($ele[$now], 800 );
+ }
+ break;
+ }
+ return;
+ });
+});
+
diff --git a/app/media/resize.py b/app/media/resize.py
new file mode 100644
index 0000000..13c0151
--- /dev/null
+++ b/app/media/resize.py
@@ -0,0 +1,53 @@
+import os
+import io
+
+from django.conf import settings
+
+try:
+ import Image
+ import ImageFile
+except ImportError:
+ try:
+ from PIL import Image
+ from PIL import ImageFile
+ except ImportError:
+ raise ImportError("Could not import the Python Imaging Library.")
+
+ImageFile.MAXBLOCK = 1000000
+
+
+def make_local_copies(self,photo):
+ orig_dir = settings.IMAGES_ROOT + '/flickr/full/' + photo.pub_date.strftime("%Y")
+ if not os.path.isdir(orig_dir):
+ os.makedirs(orig_dir)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_full = '%s/%s.jpg' % (orig_dir, photo.flickr_id)
+ img.save(local_full)
+
+ #calculate crop:
+ cur_width, cur_height = img.size
+ new_width, new_height = 291, 350
+ ratio = max(float(new_width) / cur_width, float(new_height) / cur_height)
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ xd = abs(new_width - x)
+ yd = abs(new_height - y)
+ x_diff = int(xd / 2)
+ y_diff = int(yd / 2)
+ box = (int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height))
+
+ # create resized file
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
+ # save resized file
+ resized_filename = '%s/%s.jpg' % (crop_dir, set.id)
+ try:
+ if img.format == 'JPEG':
+ resized.save(resized_filename, 'JPEG', quality=95, optimize=True)
+ else:
+ resized.save(resized_filename)
+ except IOError as e:
+ if os.path.isfile(resized_filename):
+ os.unlink(resized_filename)
+ raise e
+ os.unlink(img)
diff --git a/app/media/retriever.py b/app/media/retriever.py
new file mode 100644
index 0000000..f5cae68
--- /dev/null
+++ b/app/media/retriever.py
@@ -0,0 +1,323 @@
+import json
+import datetime
+import os
+import io
+import urllib.request
+import urllib.parse
+import urllib.error
+
+from django.template.defaultfilters import slugify
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.encoding import force_text
+from django.conf import settings
+
+from photos.models import Photo, PhotoGallery
+
+# from https://github.com/alexis-mignon/python-flickr-api
+# terribly documented, but offers a good clean OOP approach if you're willing to figure it out...
+import flickr_api
+import flickrapi
+
+# Required PIL classes may or may not be available from the root namespace depending on the installation
+try:
+ import Image
+ import ImageFile
+except ImportError:
+ try:
+ from PIL import Image
+ from PIL import ImageFile
+ except ImportError:
+ raise ImportError("Could not import the Python Imaging Library.")
+
+ImageFile.MAXBLOCK = 1000000
+
+EXIF_PARAMS = {
+ "FNumber": 'f/2.8',
+ "Make": 'Apple',
+ "Model": 'iPhone',
+ "ExposureTime": '',
+ "ISO": '',
+ "FocalLength": '',
+ "LensModel": '',
+ 'DateTimeOriginal': '2013:09:03 22:44:25'
+}
+
+class SyncFlickr():
+
+ def __init__(self):
+ self.flickr = flickrapi.FlickrAPI(settings.FLICKR_API_KEY, settings.FLICKR_API_SECRET,format='parsed-json')
+
+
+ def sync_sets(self, *args, **kwargs):
+ p = self.flickr.photosets.getList(user_id='85322932@N00')
+ disregard = [
+ 'POTD 2008',
+ 'Snow Day',
+ 'Wedding',
+ 'Some random stuff',
+ 'Lilah & Olivia',
+ '6 months+',
+ '6-9 months',
+ '9-18 months',
+ ]
+ for photoset in p['photosets']['photoset']:
+ if photoset['title']['_content'] in disregard:
+ pass
+ else:
+ try:
+ row = PhotoGallery.objects.get(set_id__exact=photoset['id'])
+ print(('%s %s %s' % ('already have', row.set_title, 'moving on...')))
+ # okay it already exists, but is it up-to-date?
+ self.get_photos_in_set(photoset['id'],row)
+ except ObjectDoesNotExist:
+ s = PhotoGallery.objects.get_or_create(
+ set_id=force_text(photoset['id']),
+ set_title=force_text(photoset['title']['_content']),
+ set_desc=force_text(photoset['description']['_content']),
+ set_slug=slugify(force_text(photoset['title']['_content'])[:40]),
+ primary=force_text(photoset['primary']),
+ pub_date=datetime.datetime.fromtimestamp(float(photoset['date_create']))
+ )
+
+ #get_photos_in_set(photoset, s)
+ #create the gallery thumbnail image:
+ #photo = Photo.objects.get(flickr_id__exact=str(photoset['primary']))
+ #make_gallery_thumb(photo, s)
+
+
+
+ def get_photos_in_set(self, flickr_id, photoset):
+ photos = self.flickr.photosets.getPhotos(photoset_id=flickr_id)
+ for photo in photos['photoset']['photo']:
+ try:
+ p = Photo.objects.get(flickr_id__exact=str(photo['id']))
+ except ObjectDoesNotExist:
+ p = self.get_photo(photo['id'])
+ if p.is_public:
+ pass #photoset.photos.add(p)
+ #slideshow_image(p, 1000, 800, 95)
+ print(p)
+
+ def get_photo(self, photo_id):
+ photo = self.flickr.photos.getInfo(photo_id=photo_id)
+ info = photo['photo']
+ try:
+ geo = self.flickr.photos.geo.getLocation(photo_id=photo_id)
+ location, region = self.get_geo(float(geo['photo']['location']['latitude']), float(geo['photo']['location']['longitude']))
+ except KeyError:
+ print("no effing geodata asshat")
+ exif = self.exif_handler(self.flickr.photos.getExif(photo_id=photo_id)['photo']['exif'])
+ p, created = Photo.objects.get_or_create(
+ title=info['title']['_content'],
+ flickr_id=info['id'],
+ flickr_owner=info['owner']['nsid'],
+ flickr_server=info['server'],
+ flickr_secret=info['secret'],
+ flickr_originalsecret=info['originalsecret'],
+ flickr_farm=info['farm'],
+ pub_date=self.flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)),
+ description=info['description']['_content'],
+ exif_aperture=exif['FNumber'],
+ exif_make=exif['Make'],
+ exif_model=exif['Model'],
+ exif_exposure=exif['ExposureTime'],
+ exif_iso=exif['ISO'],
+ exif_lens=exif['LensModel'],
+ exif_focal_length=exif['FocalLength'],
+ exif_date=self.flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)),
+ lat=float(geo['photo']['location']['latitude']),
+ lon=float(geo['photo']['location']['longitude']),
+ region=region,
+ location=location,
+ )
+ if created:
+ for tag in info['tags']['tag']:
+ p.tags.add(tag['raw'])
+ p.save()
+
+ local = FlickrImage()
+ local.make_local_copies(p)
+ #retina image:
+ #slideshow_image(p, 2000, 1600, 75)
+ #normal image
+ print("grabbing... "+p.title)
+ return p
+
+
+ def sync_flickr_photos(self, *args, **kwargs):
+ photos = self.flickr.people.getPhotos(user_id="85322932@N00", extras="date_upload,date_taken,geo")
+ for photo in photos['photos']['photo']:
+ try:
+ row = Photo.objects.get(flickr_id=photo['id'], flickr_secret=photo['secret'])
+ print('already have ' + photo['id'] + ' moving on')
+ except ObjectDoesNotExist:
+ p = self.get_photo(photo['id'])
+
+
+
+ """
+ ################################################
+ ## Various meta data and geo helper functions ##
+ ################################################
+ """
+
+ def exif_handler(self, data):
+ converted = {}
+ try:
+ for t in data:
+ converted[t['tag']] = t['raw']['_content']
+ except:
+ pass
+ for k, v in list(EXIF_PARAMS.items()):
+ if k not in converted:
+ converted[k] = v
+ return converted
+
+
+ def flickr_datetime_to_datetime(self, fdt):
+ from datetime import datetime
+ from time import strptime
+ date_parts = strptime(fdt, '%Y-%m-%d %H:%M:%S')
+ return datetime(*date_parts[0:6])
+
+
+ def get_geo(self, lat, lon):
+ from locations.models import Location, Region
+ from django.contrib.gis.geos import Point
+ pnt_wkt = Point(lon, lat)
+ try:
+ location = Location.objects.get(geometry__contains=pnt_wkt)
+ except Location.DoesNotExist:
+ location = None
+ try:
+ region = Region.objects.get(geometry__contains=pnt_wkt)
+ except Region.DoesNotExist:
+ region = None
+ return location, region
+
+
+
+
+
+
+class FlickrImage():
+ """
+ ## Photo retrieval functions to pull down images from Flickr servers ##
+ """
+
+ def slideshow_image(self, photo, max_width, max_height, quality):
+ slide_dir = settings.IMAGES_ROOT + '/slideshow/' + photo.pub_date.strftime("%Y")
+ if not os.path.isdir(slide_dir):
+ os.makedirs(slide_dir)
+
+ # Is it a retina image or not?
+ if max_width >= 1001 or max_height >= 801:
+ filename = '%s/%sx2.jpg' % (slide_dir, photo.flickr_id)
+ else:
+ filename = '%s/%s.jpg' % (slide_dir, photo.flickr_id)
+
+ flickr_photo = photo.get_original_url()
+ fname = urllib.request.urlopen(flickr_photo)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ cur_width, cur_height = img.size
+ #if image landscape
+ if cur_width > cur_height:
+ new_width = max_width
+ #check to make sure we aren't upsizing
+ if cur_width > new_width:
+ ratio = float(new_width) / cur_width
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ else:
+ #image portrait
+ new_height = max_height
+ #check to make sure we aren't upsizing
+ if cur_height > new_height:
+ ratio = float(new_height) / cur_height
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ photo.slideshowimage_width = photo.get_width
+ photo.slideshowimage_height = photo.get_height
+ photo.slideshowimage_margintop = photo.get_margin_top
+ photo.slideshowimage_marginleft = photo.get_margin_left
+ photo.save()
+ #now resize the local copy
+
+
+ def make_local_copies(self,photo):
+ orig_dir = settings.IMAGES_ROOT + '/flickr/full/' + photo.pub_date.strftime("%Y")
+ if not os.path.isdir(orig_dir):
+ os.makedirs(orig_dir)
+ full = photo.get_original_url()
+ fname = urllib.request.urlopen(full)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_full = '%s/%s.jpg' % (orig_dir, photo.flickr_id)
+ img.save(local_full)
+ #save large size
+ large_dir = settings.IMAGES_ROOT + '/flickr/large/' + photo.pub_date.strftime("%Y")
+ if not os.path.isdir(large_dir):
+ os.makedirs(large_dir)
+ large = photo.get_large_url()
+ fname = urllib.request.urlopen(large)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_large = '%s/%s.jpg' % (large_dir, photo.flickr_id)
+ if img.format == 'JPEG':
+ img.save(local_large)
+ #save medium size
+ med_dir = settings.IMAGES_ROOT + '/flickr/med/' + photo.pub_date.strftime("%Y")
+ if not os.path.isdir(med_dir):
+ os.makedirs(med_dir)
+ med = photo.get_medium_url()
+ fname = urllib.request.urlopen(med)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_med = '%s/%s.jpg' % (med_dir, photo.flickr_id)
+ img.save(local_med)
+
+
+ def make_gallery_thumb(self, photo, set):
+ crop_dir = settings.IMAGES_ROOT + '/gallery_thumbs/'
+ if not os.path.isdir(crop_dir):
+ os.makedirs(crop_dir)
+ remote = photo.get_original_url()
+ print(remote)
+ fname = urllib.request.urlopen(remote)
+ im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image
+ img = Image.open(im)
+ #calculate crop:
+ cur_width, cur_height = img.size
+ new_width, new_height = 291, 350
+ ratio = max(float(new_width) / cur_width, float(new_height) / cur_height)
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ xd = abs(new_width - x)
+ yd = abs(new_height - y)
+ x_diff = int(xd / 2)
+ y_diff = int(yd / 2)
+ box = (int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height))
+
+ # create resized file
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
+ # save resized file
+ resized_filename = '%s/%s.jpg' % (crop_dir, set.id)
+ try:
+ if img.format == 'JPEG':
+ resized.save(resized_filename, 'JPEG', quality=95, optimize=True)
+ else:
+ resized.save(resized_filename)
+ except IOError as e:
+ if os.path.isfile(resized_filename):
+ os.unlink(resized_filename)
+ raise e
+ os.unlink(img)
diff --git a/app/media/retriever.py.bak b/app/media/retriever.py.bak
new file mode 100644
index 0000000..d3c572a
--- /dev/null
+++ b/app/media/retriever.py.bak
@@ -0,0 +1,314 @@
+from __future__ import division
+import datetime
+import os
+import cStringIO
+import urllib
+
+from django.template.defaultfilters import slugify
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.encoding import force_unicode
+from django.conf import settings
+
+# Required PIL classes may or may not be available from the root namespace
+# depending on the installation
+try:
+ import Image
+ import ImageFile
+except ImportError:
+ try:
+ from PIL import Image
+ from PIL import ImageFile
+ except ImportError:
+ raise ImportError("Could not import the Python Imaging Library.")
+
+ImageFile.MAXBLOCK = 1000000
+
+from photos.models import Photo, PhotoGallery
+
+# from https://github.com/alexis-mignon/python-flickr-api
+# terribly documented, but offers a good clean OOP approach if you're willing to figure it out...
+import flickr_api
+
+EXIF_PARAMS = {
+ "FNumber": 'f/2.8',
+ "Make": 'Apple',
+ "Model": 'iPhone',
+ "ExposureTime": '',
+ "ISO": '',
+ "FocalLength": '',
+ "LensModel": '',
+ 'DateTimeOriginal': '2013:09:03 22:44:25'
+}
+
+
+def sync_flickr_photos(*args, **kwargs):
+ flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET)
+ flickr_api.set_auth_handler("app/photos/flickrauth")
+ user = flickr_api.test.login()
+ photos = user.getPhotos(extras="date_upload,date_taken,geo")
+ # reverse! reverse!
+ photos.reverse()
+ for photo in photos:
+ info = photo.getInfo()
+ try:
+ row = Photo.objects.get(flickr_id=info['id'], flickr_secret=info['secret'])
+ print('already have ' + info['id'] + ' moving on')
+ except ObjectDoesNotExist:
+ get_photo(photo)
+
+
+def get_photo(photo):
+ info = photo.getInfo()
+ geo = photo.getLocation()
+ location, region = get_geo(float(geo['latitude']), float(geo['longitude']))
+ exif = exif_handler(photo.getExif())
+ p, created = Photo.objects.get_or_create(
+ title=info['title'],
+ flickr_id=info['id'],
+ flickr_owner=info['owner']['id'],
+ flickr_server=info['server'],
+ flickr_secret=info['secret'],
+ flickr_originalsecret=info['originalsecret'],
+ flickr_farm=info['farm'],
+ pub_date=flickr_datetime_to_datetime(info['taken']),
+ description=info['description'],
+ exif_aperture=exif['FNumber'],
+ exif_make=exif['Make'],
+ exif_model=exif['Model'],
+ exif_exposure=exif['ExposureTime'],
+ exif_iso=exif['ISO'],
+ exif_lens=exif['LensModel'],
+ exif_focal_length=exif['FocalLength'],
+ exif_date=flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)),
+ lat=float(geo['latitude']),
+ lon=float(geo['longitude']),
+ region=region,
+ location=location,
+ )
+ if created:
+ for tag in info['tags']:
+ p.tags.add(tag['raw'])
+ p.save()
+ make_local_copies(p)
+ #retina image:
+ #slideshow_image(p, 2000, 1600, 75)
+ #normal image
+ print(p.title)
+ return p
+
+
+def sync_sets(*args, **kwargs):
+ flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET)
+ flickr_api.set_auth_handler("app/photos/flickrauth")
+ user = flickr_api.test.login()
+ photosets = user.getPhotosets()
+ # reverse! reverse!
+ photosets.reverse()
+ disregard = [
+ 'POTD 2008',
+ 'Snow Day',
+ 'Wedding',
+ 'Some random stuff',
+ 'Lilah & Olivia',
+ '6 months+',
+ '6-9 months',
+ '9-18 months',
+ ]
+ for photoset in photosets:
+ if photoset['title'] in disregard:
+ pass
+ else:
+ try:
+ row = PhotoGallery.objects.get(set_id__exact=photoset['id'])
+ print('%s %s %s' % ('already have', row.set_title, 'moving on...'))
+ # okay it already exists, but is it up-to-date?
+ #get_photos_in_set(row,set.id)
+ except ObjectDoesNotExist:
+ s = PhotoGallery.objects.create(
+ set_id=force_unicode(photoset['id']),
+ set_title=force_unicode(photoset['title']),
+ set_desc=force_unicode(photoset['description']),
+ set_slug=slugify(force_unicode(photoset['title'])),
+ primary=force_unicode(photoset['primary']),
+ pub_date=datetime.datetime.fromtimestamp(float(photoset['date_create']))
+ )
+
+ get_photos_in_set(photoset, s)
+ #create the gallery thumbnail image:
+ photo = Photo.objects.get(flickr_id__exact=str(photoset['primary']))
+ make_gallery_thumb(photo, s)
+
+
+def get_photos_in_set(flickr_photoset, photoset):
+ for photo in flickr_photoset.getPhotos():
+ try:
+ p = Photo.objects.get(flickr_id__exact=str(photo['id']))
+ except ObjectDoesNotExist:
+ p = get_photo(photo)
+ if p.is_public:
+ photoset.photos.add(p)
+ slideshow_image(p, 1000, 800, 95)
+
+
+################################################
+## Various meta data and geo helper functions ##
+################################################
+
+
+def exif_handler(data):
+ converted = {}
+ try:
+ for t in data:
+ converted[t['tag']] = t['raw']
+ except:
+ pass
+ for k, v in EXIF_PARAMS.items():
+ if not converted.has_key(k):
+ converted[k] = v
+ return converted
+
+
+def flickr_datetime_to_datetime(fdt):
+ from datetime import datetime
+ from time import strptime
+ date_parts = strptime(fdt, '%Y-%m-%d %H:%M:%S')
+ return datetime(*date_parts[0:6])
+
+def get_geo(lat,lon):
+ from locations.models import Location, Region
+ from django.contrib.gis.geos import Point
+ pnt_wkt = Point(lon, lat)
+ try:
+ location = Location.objects.get(geometry__contains=pnt_wkt)
+ except Location.DoesNotExist:
+ location = None
+ try:
+ region = Region.objects.get(geometry__contains=pnt_wkt)
+ except Region.DoesNotExist:
+ region = None
+ return location, region
+
+#######################################################################
+## Photo retrieval functions to pull down images from Flickr servers ##
+#######################################################################
+
+def slideshow_image(photo,max_width, max_height, quality):
+ slide_dir = settings.IMAGES_ROOT + '/slideshow/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(slide_dir):
+ os.makedirs(slide_dir)
+
+ # Is it a retina image or not?
+ if max_width >= 1001 or max_height >= 801:
+ filename = '%s/%sx2.jpg' %(slide_dir, photo.flickr_id)
+ else:
+ filename = '%s/%s.jpg' %(slide_dir, photo.flickr_id)
+
+ flickr_photo = photo.get_original_url()
+ fname = urllib.urlopen(flickr_photo)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ cur_width, cur_height = img.size
+ #if image landscape
+ if cur_width > cur_height:
+ new_width = max_width
+ #check to make sure we aren't upsizing
+ if cur_width > new_width:
+ ratio = float(new_width)/cur_width
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ else:
+ #image portrait
+ new_height = max_height
+ #check to make sure we aren't upsizing
+ if cur_height > new_height:
+ ratio = float(new_height)/cur_height
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ photo.slideshowimage_width = photo.get_width
+ photo.slideshowimage_height = photo.get_height
+ photo.slideshowimage_margintop = photo.get_margin_top
+ photo.slideshowimage_marginleft = photo.get_margin_left
+ photo.save()
+ #now resize the local copy
+
+
+
+def make_local_copies(photo):
+ orig_dir = settings.IMAGES_ROOT + '/flickr/full/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(orig_dir):
+ os.makedirs(orig_dir)
+ full = photo.get_original_url()
+ fname = urllib.urlopen(full)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_full = '%s/%s.jpg' %(orig_dir, photo.flickr_id)
+ img.save(local_full)
+ #save large size
+ large_dir = settings.IMAGES_ROOT + '/flickr/large/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(large_dir):
+ os.makedirs(large_dir)
+ large = photo.get_large_url()
+ fname = urllib.urlopen(large)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_large = '%s/%s.jpg' %(large_dir, photo.flickr_id)
+ if img.format == 'JPEG':
+ img.save(local_large)
+ #save medium size
+ med_dir = settings.IMAGES_ROOT + '/flickr/med/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(med_dir):
+ os.makedirs(med_dir)
+ med = photo.get_medium_url()
+ fname = urllib.urlopen(med)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_med = '%s/%s.jpg' %(med_dir, photo.flickr_id)
+ img.save(local_med)
+
+def make_gallery_thumb(photo,set):
+ crop_dir = settings.IMAGES_ROOT + '/gallery_thumbs/'
+ if not os.path.isdir(crop_dir):
+ os.makedirs(crop_dir)
+ remote = photo.get_original_url()
+ print(remote)
+ fname = urllib.urlopen(remote)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+
+ #calculate crop:
+ cur_width, cur_height = img.size
+ new_width, new_height = 291, 350
+ ratio = max(float(new_width)/cur_width,float(new_height)/cur_height)
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ xd = abs(new_width - x)
+ yd = abs(new_height - y)
+ x_diff = int(xd / 2)
+ y_diff = int(yd / 2)
+ box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height))
+
+ #create resized file
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
+ # save resized file
+ resized_filename = '%s/%s.jpg' %(crop_dir, set.id)
+ try:
+ if img.format == 'JPEG':
+ resized.save(resized_filename, 'JPEG', quality=95, optimize=True)
+ else:
+ resized.save(resized_filename)
+ except IOError, e:
+ if os.path.isfile(resized_filename):
+ os.unlink(resized_filename)
+ raise e
+ #os.unlink(img)
+
+
+
diff --git a/app/media/static/image-preview.js b/app/media/static/image-preview.js
new file mode 100644
index 0000000..b8fead5
--- /dev/null
+++ b/app/media/static/image-preview.js
@@ -0,0 +1,42 @@
+function build_image_preview () {
+ var url = window.location.href
+ var cur = url.split('/')[6];
+ if (cur) {
+ var container = document.createElement("div");
+ container.className = "form-row field-image";
+ var wrapper = document.createElement("div");
+ var label = document.createElement("label");
+ label.textContent = "Image:";
+ var pwrap = document.createElement("p");
+ var img = document.createElement("img");
+
+ var request = new XMLHttpRequest();
+ request.open('GET', '/photos/luximage/data/admin/preview/'+cur+'/', true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ //console.log(data);
+ img.src = data['url'];
+ } else {
+ console.log("server error");
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ pwrap.appendChild(img);
+ wrapper.appendChild(label);
+ wrapper.appendChild(pwrap);
+ container.appendChild(wrapper);
+ parent = document.getElementById("luximage_form");
+ node = parent.children[1].children[0];
+ node.parentNode.insertBefore(container, node.previousSibling);
+ } else {
+ return;
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function(event) {
+ build_image_preview();
+});
diff --git a/app/media/static/my_styles.css b/app/media/static/my_styles.css
new file mode 100644
index 0000000..d13c8e4
--- /dev/null
+++ b/app/media/static/my_styles.css
@@ -0,0 +1,40 @@
+
+/*o.v.*/
+
+#id_featured_image {
+ /*style the "box" in its minimzed state*/
+ border:1px solid black; width:230px; overflow:hidden;
+ height:300px; overflow-y:scroll;
+ /*animate collapsing the dropdown from open to closed state (v. fast)*/
+}
+#id_featured_image input {
+ /*hide the nasty default radio buttons. like, completely!*/
+ position:absolute;top:0;left:0;opacity:0;
+}
+
+
+#id_featured_image label {
+ /*style the labels to look like dropdown options, kinda*/
+ color: #000;
+ display:block;
+ margin: 2px 2px 2px 10px;
+ height:102px;
+ opacity:.6;
+ background-repeat: no-repeat;
+}
+#id_featured_image:hover label{
+ /*this is how labels render in the "expanded" state. we want to see only the selected radio button in the collapsed menu, and all of them when expanded*/
+}
+#id_featured_image label:hover {
+ opacity:.8;
+}
+#id_featured_image input:checked + label {
+ /*tricky! labels immediately following a checked radio button (with our markup they are semantically related) should be fully opaque regardless of hover, and they should always be visible (i.e. even in the collapsed menu*/
+ opacity:1 !important;
+ display:block;
+ background: #333;
+}
+
+/*pfft, nothing as cool here, just the value trace*/
+#trace {margin:0 0 20px;}
+#id_featured_image li:first-child { display: none;}
diff --git a/app/media/templatetags/__init__.py b/app/media/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/media/templatetags/__init__.py
diff --git a/app/media/templatetags/get_image_by_size.py b/app/media/templatetags/get_image_by_size.py
new file mode 100644
index 0000000..c56c44e
--- /dev/null
+++ b/app/media/templatetags/get_image_by_size.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def get_image_by_size(obj, *args):
+ method = getattr(obj, "get_image_by_size")
+ return method(*args)
diff --git a/app/media/templatetags/get_image_width.py b/app/media/templatetags/get_image_width.py
new file mode 100644
index 0000000..ac39184
--- /dev/null
+++ b/app/media/templatetags/get_image_width.py
@@ -0,0 +1,9 @@
+from math import floor
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def get_image_width(obj, size, *args):
+ ratio = floor(int(size)*100/int(obj.height))/100
+ return floor(ratio*int(obj.height))
diff --git a/app/media/templatetags/get_size_by_name.py b/app/media/templatetags/get_size_by_name.py
new file mode 100644
index 0000000..fc64a61
--- /dev/null
+++ b/app/media/templatetags/get_size_by_name.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def get_size_by_name(obj, *args):
+ method = getattr(obj, "get_size_by_name")
+ return method(*args)
diff --git a/app/media/urls.py b/app/media/urls.py
new file mode 100644
index 0000000..6673135
--- /dev/null
+++ b/app/media/urls.py
@@ -0,0 +1,74 @@
+from django.urls import path, re_path
+from django.views.generic.base import RedirectView
+
+from . import views
+
+app_name = "photos"
+
+urlpatterns = [
+ path(
+ r'daily/<int:page>',
+ views.DailyPhotoList.as_view(),
+ name="daily_photo_list"
+ ),
+ path(
+ r'daily/',
+ views.DailyPhotoList.as_view(),
+ {'page': 1},
+ name="daily_photo_list"
+ ),
+ path(
+ r'data/(<str:slug>/',
+ views.photo_json
+ ),
+ re_path(
+ r'data/admin/preview/(?P<pk>\d+)/$',
+ views.photo_preview_json,
+ name="admin_image_preview"
+ ),
+ re_path(
+ r'data/admin/tn/(?P<pk>\d+)/$',
+ views.thumb_preview_json,
+ name="admin_thumb_preview"
+ ),
+ re_path(
+ r'galleries/private/(?P<slug>[-\w]+)$',
+ views.PrivateGallery.as_view(),
+ name="private"
+ ),
+ re_path(
+ r'galleries/private/(?P<page>\d+)/$',
+ views.PrivateGalleryList.as_view(),
+ name="private_list"
+ ),
+ re_path(
+ r'galleries/private/$',
+ RedirectView.as_view(url="/photos/galleries/private/1/", permanent=False)
+ ),
+ re_path(
+ r'galleries/(?P<slug>[-\w]+)$',
+ views.Gallery.as_view(),
+ name="private"
+ ),
+ re_path(
+ r'galleries/(?P<page>\d+)/$',
+ views.GalleryList.as_view(),
+ name="private_list"
+ ),
+ re_path(
+ r'galleries/$',
+ RedirectView.as_view(url="/photos/galleries/1/", permanent=False)
+ ),
+ re_path(
+ r'(?P<page>\d+)/$',
+ views.gallery_list,
+ ),
+ re_path(
+ r'(?P<slug>[-\w]+)/$',
+ RedirectView.as_view(url="/photos/%(slug)s/1/", permanent=False)
+ ),
+ re_path(
+ r'',
+ RedirectView.as_view(url="/photos/1/", permanent=False)
+ ),
+]
diff --git a/app/media/utils.py b/app/media/utils.py
new file mode 100644
index 0000000..84e72f5
--- /dev/null
+++ b/app/media/utils.py
@@ -0,0 +1,28 @@
+import os
+import re
+import subprocess
+
+from django.apps import apps
+from django.conf import settings
+
+from PIL import ImageFile
+from bs4 import BeautifulSoup
+# pip install python-resize-image
+from resizeimage import resizeimage
+
+
+def resize_image(img, width=None, height=None, quality=72, base_path="", filename=""):
+ if width and height:
+ newimg = resizeimage.resize_cover(img, [width, height])
+ if width and not height:
+ newimg = resizeimage.resize_width(img, width)
+ if height and not width:
+ newimg = resizeimage.resize_height(img, height)
+ if not os.path.isdir(base_path):
+ os.makedirs(base_path)
+ path = "%s%s" % (base_path, filename)
+ ImageFile.MAXBLOCK = img.size[0] * img.size[1] * 4
+ newimg.save(path, newimg.format, quality=quality)
+ subprocess.call(["jpegoptim", "%s" % path])
+
+
diff --git a/app/media/views.py b/app/media/views.py
new file mode 100644
index 0000000..915b022
--- /dev/null
+++ b/app/media/views.py
@@ -0,0 +1,130 @@
+import json
+from django.shortcuts import render_to_response, render
+from django.template import RequestContext
+from django.http import Http404, HttpResponse
+from django.core import serializers
+
+from .models import Photo, PhotoGallery, LuxGallery, LuxImage
+from locations.models import Country, Region
+
+from utils.views import PaginatedListView
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+
+
+class PrivateGallery(DetailView):
+ model = LuxGallery
+ slug_field = "slug"
+ template_name = "details/photo_gallery.html"
+
+
+class PrivateGalleryList(PaginatedListView):
+ template_name = 'archives/gallery_list.html'
+
+ def get_queryset(self):
+ return LuxGallery.objects.filter(is_public=False)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(PrivateGalleryList, self).get_context_data(**kwargs)
+ context['is_private'] = True
+ return context
+
+
+class Gallery(DetailView):
+ model = LuxGallery
+ slug_field = "slug"
+ template_name = "details/photo_gallery.html"
+
+
+class GalleryList(PaginatedListView):
+ template_name = 'archives/gallery_list.html'
+
+ def get_queryset(self):
+ return LuxGallery.objects.filter(is_public=True)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(GalleryList, self).get_context_data(**kwargs)
+ context['is_private'] = False
+ return context
+
+
+class OldGalleryList(PaginatedListView):
+ template_name = 'archives/gallery_list.html'
+ model = PhotoGallery
+
+ def get_queryset(self):
+ return PhotoGallery.objects.filter(is_public=True)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(OldGalleryList, self).get_context_data(**kwargs)
+ return context
+
+
+class DailyPhotoList(PaginatedListView):
+ template_name = 'archives/photo_daily_list.html'
+
+ def get_queryset(self):
+ return LuxImage.objects.filter(is_public=True, title__startswith="daily_")
+
+
+def gallery_list(request, page):
+ request.page_url = '/photos/%d/'
+ request.page = int(page)
+ context = {
+ 'object_list': PhotoGallery.objects.all(),
+ 'page': page,
+ }
+ return render(request, "archives/photos.html", context)
+
+
+def gallery(request, slug):
+ context = {
+ 'object': PhotoGallery.objects.get(set_slug=slug)
+ }
+ return render_to_response('details/photo_galleries.html', context, context_instance=RequestContext(request))
+
+
+def photo_json(request, slug):
+ p = PhotoGallery.objects.filter(set_slug=slug)
+ return HttpResponse(serializers.serialize('json', p), mimetype='application/json')
+
+
+def photo_preview_json(request, pk):
+ p = LuxImage.objects.get(pk=pk)
+ data = {}
+ data['url'] = p.get_admin_image()
+ data = json.dumps(data)
+ return HttpResponse(data)
+
+
+def thumb_preview_json(request, pk):
+ p = LuxImage.objects.get(pk=pk)
+ data = {}
+ data['url'] = p.get_admin_insert()
+ data = json.dumps(data)
+ return HttpResponse(data)
+
+
+def gallery_list_by_area(request, slug, page):
+ """Grabs entries by region or country"""
+ request.page_url = '/photos/' + slug + '/%d/'
+ request.page = int(page)
+ try:
+ region = Region.objects.get(slug__exact=slug)
+ qs = PhotoGallery.objects.filter(region=region).order_by('-id')
+ except:
+ region = Country.objects.get(slug__exact=slug)
+ qs = PhotoGallery.objects.filter(location__state__country=region).order_by('-id')
+ if not region:
+ raise Http404
+ context = {
+ 'object_list': qs,
+ 'country_list': Country.objects.filter(visited=True),
+ 'region_list': Region.objects.all(),
+ 'region': region,
+ 'page': page
+ }
+ return render_to_response("archives/photos.html", context, context_instance=RequestContext(request))
diff --git a/app/normalize/__init__.py b/app/normalize/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/normalize/__init__.py
diff --git a/app/normalize/admin.py b/app/normalize/admin.py
new file mode 100644
index 0000000..cf67658
--- /dev/null
+++ b/app/normalize/admin.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+
+from utils.widgets import AdminImageWidget, LGEntryForm
+
+from .models import RelatedPost
+
+@admin.register(RelatedPost)
+class RelatedPostAdmin(admin.ModelAdmin):
+ list_display = ('title', 'slug', 'pub_date', 'model_name')
+ list_filter = ['model_name']
+
+ class Media:
+ js = ('next-prev-links.js',)
diff --git a/app/normalize/migrations/0001_initial.py b/app/normalize/migrations/0001_initial.py
new file mode 100644
index 0000000..358677a
--- /dev/null
+++ b/app/normalize/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.1.3 on 2020-11-30 22:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='RelatedPost',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('entry_id', models.IntegerField()),
+ ('title', models.CharField(max_length=200)),
+ ('slug', models.CharField(max_length=50)),
+ ('pub_date', models.DateTimeField(verbose_name='Date published')),
+ ('model_name', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.contenttype')),
+ ],
+ options={
+ 'ordering': ('-model_name', '-pub_date'),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ ]
diff --git a/app/normalize/migrations/0002_alter_relatedpost_id.py b/app/normalize/migrations/0002_alter_relatedpost_id.py
new file mode 100644
index 0000000..16f958e
--- /dev/null
+++ b/app/normalize/migrations/0002_alter_relatedpost_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('normalize', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='relatedpost',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/app/normalize/migrations/__init__.py b/app/normalize/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/normalize/migrations/__init__.py
diff --git a/app/normalize/models.py b/app/normalize/models.py
new file mode 100644
index 0000000..f45f90b
--- /dev/null
+++ b/app/normalize/models.py
@@ -0,0 +1,17 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+
+
+class RelatedPost(models.Model):
+ model_name = models.ForeignKey(ContentType, null=True, on_delete=models.SET_NULL)
+ entry_id = models.IntegerField()
+ title = models.CharField(max_length=200)
+ slug = models.CharField(max_length=50)
+ pub_date = models.DateTimeField('Date published')
+
+ class Meta:
+ ordering = ('-model_name', '-pub_date',)
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ return "%s - %s" % (self.model_name, self.title)
diff --git a/app/pages/__init__.py b/app/pages/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/pages/__init__.py
diff --git a/app/pages/admin.py b/app/pages/admin.py
new file mode 100644
index 0000000..3c7d900
--- /dev/null
+++ b/app/pages/admin.py
@@ -0,0 +1,55 @@
+from django.contrib import admin
+from utils.widgets import LGEntryForm
+
+from django import forms
+from django.forms import Textarea
+from django.db import models
+
+from pages.models import Page, HomePage
+
+
+class PageEntryForm(forms.ModelForm):
+ class Meta:
+ model = Page
+ fields = '__all__'
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 50, 'cols': 100}),
+ }
+
+
+@admin.register(Page)
+class PageAdmin(admin.ModelAdmin):
+ form = LGEntryForm
+ list_display = ('title', 'site', 'slug', 'path', 'app', 'build')
+ search_fields = ['title', 'body_markdown']
+ prepopulated_fields = {"slug": ('title',)}
+ fieldsets = (
+ ('Page', {
+ 'fields': ('title', 'sub_title', 'body_markdown', ('build', 'enable_comments'), ('site','slug', 'path', 'app'), 'featured_image', 'pub_date'),
+ 'classes': ('show', 'extrapretty', 'wide')
+ }),
+ ('Metadata', {
+ 'classes': ('collapse closed',),
+ 'fields': ('meta_description',),
+ })
+ )
+
+ class Media:
+ js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
+
+
+
+
+@admin.register(HomePage)
+class HomePageAdmin(admin.ModelAdmin):
+ form = LGEntryForm
+ filter_horizontal = ('popular',)
+
+ class Media:
+ js = ('image-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
diff --git a/app/pages/build.py b/app/pages/build.py
new file mode 100644
index 0000000..9545ce3
--- /dev/null
+++ b/app/pages/build.py
@@ -0,0 +1,25 @@
+from django.template.loader import render_to_string
+from django.template import Context
+from django.urls import reverse
+from django.conf import settings
+
+from builder.base import BuildNew
+
+
+class BuildPages(BuildNew):
+
+ def build(self):
+ self.build_detail_view()
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(build=True,site=self.site)
+
+
+class BuildHome(BuildNew):
+
+ def get_model_queryset(self):
+ return self.model.objects.get(pk=1)
+
+ def build(self):
+ response = self.client.get('/')
+ self.write_file('/', response.content)
diff --git a/app/pages/migrations/0001_initial.py b/app/pages/migrations/0001_initial.py
new file mode 100644
index 0000000..fe5ef96
--- /dev/null
+++ b/app/pages/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:30
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('sites', '0002_alter_domain_unique'),
+ ('media', '__first__'),
+ ('posts', '__first__'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Page',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('sub_title', models.CharField(blank=True, max_length=300)),
+ ('slug', models.SlugField()),
+ ('body_html', models.TextField(blank=True)),
+ ('body_markdown', models.TextField()),
+ ('meta_description', models.CharField(blank=True, max_length=256)),
+ ('path', models.CharField(blank=True, max_length=200)),
+ ('app', models.CharField(blank=True, max_length=50)),
+ ('build', models.BooleanField(default=True)),
+ ('enable_comments', models.BooleanField(default=False)),
+ ('pub_date', models.DateTimeField(blank=True, null=True, verbose_name='Date published')),
+ ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')),
+ ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='HomePage',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('image_offset_vertical', models.CharField(help_text='add negative top margin to shift image (include css unit)', max_length=20)),
+ ('tag_line', models.CharField(blank=True, max_length=200, null=True)),
+ ('template_name', models.CharField(blank=True, help_text='full path', max_length=200, null=True)),
+ ('featured', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='banner', to='posts.post')),
+ ('featured_image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='media.luximage')),
+ ('popular', models.ManyToManyField(related_name='popular', to='posts.Post')),
+ ],
+ ),
+ ]
diff --git a/app/pages/migrations/0002_auto_20211031_1354.py b/app/pages/migrations/0002_auto_20211031_1354.py
new file mode 100644
index 0000000..8fa8e90
--- /dev/null
+++ b/app/pages/migrations/0002_auto_20211031_1354.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.2.8 on 2021-10-31 13:54
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('posts', '0001_initial'),
+ ('pages', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='homepage',
+ name='featured',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner', to='posts.post'),
+ ),
+ migrations.AlterField(
+ model_name='homepage',
+ name='image_offset_vertical',
+ field=models.CharField(blank=True, help_text='add negative top margin to shift image (include css unit)', max_length=20, null=True),
+ ),
+ migrations.AlterField(
+ model_name='homepage',
+ name='popular',
+ field=models.ManyToManyField(blank=True, null=True, related_name='popular', to='posts.Post'),
+ ),
+ ]
diff --git a/app/pages/migrations/__init__.py b/app/pages/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/pages/migrations/__init__.py
diff --git a/app/pages/models.py b/app/pages/models.py
new file mode 100644
index 0000000..7198d59
--- /dev/null
+++ b/app/pages/models.py
@@ -0,0 +1,61 @@
+import re
+from django.db import models
+from django.contrib.sitemaps import Sitemap
+from django.contrib.sites.models import Site
+
+from media.models import LuxImage
+from posts.models import Post
+from utils.util import markdown_to_html, render_images
+
+
+class Page(models.Model):
+ title = models.CharField(max_length=200)
+ sub_title = models.CharField(max_length=300, blank=True)
+ slug = models.SlugField()
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField()
+ meta_description = models.CharField(max_length=256, blank=True)
+ path = models.CharField(max_length=200, blank=True)
+ app = models.CharField(max_length=50, blank=True)
+ build = models.BooleanField(default=True)
+ enable_comments = models.BooleanField(default=False)
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+ featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True)
+ pub_date = models.DateTimeField('Date published', null=True, blank=True)
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ if self.path:
+ return "/%s/%s" % (self.path, self.slug)
+ else:
+ return "/%s" % (self.slug)
+
+ def save(self):
+ # run markdown
+ md = render_images(self.body_markdown)
+ self.body_html = markdown_to_html(md)
+ super(Page, self).save()
+
+
+class HomePage(models.Model):
+ """
+ simple model to control the featured article on the homepage
+ also allows me to fudge the "popular" section to be what I want
+ """
+ image_offset_vertical = models.CharField(max_length=20, help_text="add negative top margin to shift image (include css unit)", blank=True, null=True)
+ featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE)
+ tag_line = models.CharField(max_length=200, null=True, blank=True)
+ featured = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="banner", null=True, blank=True)
+ popular = models.ManyToManyField(Post, related_name="popular", null=True, blank=True)
+ template_name = models.CharField(max_length=200, help_text="full path", null=True, blank=True)
+
+
+class PageSitemap(Sitemap):
+ changefreq = "never"
+ priority = 1.0
+ protocol = "https"
+
+ def items(self):
+ return Page.objects.filter(build=True)
diff --git a/app/pages/static.py b/app/pages/static.py
new file mode 100644
index 0000000..3ef1a71
--- /dev/null
+++ b/app/pages/static.py
@@ -0,0 +1,123 @@
+from django.db import models
+from django.contrib.sitemaps import Sitemap
+import markdown
+
+
+import os
+import yaml
+from django.conf import settings
+from django.template.loader import render_to_string
+from django.template import Context
+from mdx_attr_list.mdx_attr_list import AttrListExtension
+
+
+def markdown_processor(txt):
+ md = markdown.Markdown(
+ extensions=[AttrListExtension(),'footnotes',],
+ output_format='html5',
+ safe_mode=False
+ )
+ return md.convert(txt)
+'''
+class Page(models.Model):
+ title = models.CharField(max_length=200)
+ slug = models.SlugField()
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField()
+ meta_description = models.CharField(max_length=256, null=True, blank=True)
+
+ def __unicode__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return "/%s/" % (self.slug)
+
+ def save(self):
+ #run markdown
+ self.body_html = markdown_processor(self.body_markdown)
+ super(Page, self).save()
+'''
+
+class PageSitemap(Sitemap):
+ changefreq = "never"
+ priority = 1.0
+ protocol = "https"
+
+ def items(self):
+ p = PageGenerator(settings.PROJ_ROOT + '_pages')
+ return p.objects(include_in_sitemap=True)
+ #return Page.objects.all()
+
+
+class PageGenerator(object):
+
+ def __init__(self, path, *args, **kwargs):
+ self._objects = []
+ for (dirpath, dirnames, filenames) in os.walk(path):
+ self.dirpath = dirpath
+ self.file_list = filter(lambda item: not (item.startswith('.') or item.endswith('~') or item.endswith('.md')), filenames)
+ self.get_files()
+
+ def get_files(self):
+ for f in self.file_list:
+ p = Page(self.dirpath + '/' + f)
+ self._objects.append(p)
+
+ def objects(self, *args, **kwargs):
+ filtered_list = []
+ if kwargs:
+ for item in self._objects:
+ found = False
+ for k, v in kwargs.items():
+ if getattr(item, k) == v and not found:
+ found = True
+ filtered_list.append(item)
+ elif getattr(item, k) != v and found:
+ filtered_list.remove(item)
+ return filtered_list
+ return self._objects
+
+ def write_files(self):
+ for obj in self.objects():
+ c = Context({'object': obj, 'SITE_URL': settings.SITE_URL})
+ t = render_to_string(["details/%s.html" % obj.template], c)
+ s = render_to_string('details/page.txt', c)
+ _FileWriter('', t, ext="html", filename=obj.slug)
+ _FileWriter('', s, ext="txt", filename=obj.slug)
+
+
+class _FileWriter(object):
+ """
+ Given a path and text object; write the page to disc
+ """
+ def __init__(self, path, text_object, ext='html', filename='index'):
+ self.path = '%s%s' % (settings.FLATFILES_ROOT, path)
+ if not os.path.isdir(self.path):
+ os.makedirs(self.path)
+ fpath = '%s%s.%s' % (self.path, filename, ext)
+ self.write(fpath, text_object)
+
+ def write(self, fpath, text_object):
+ file = open(fpath, 'wb')
+ file.write(text_object.encode('utf-8'))
+ file.close()
+
+
+class _FileLoader(object):
+
+ def __init__(self, filename, *args, **kwargs):
+ self.filename = filename
+ metadata = self.read()
+ for k, v in metadata.items():
+ setattr(self, k, v)
+ if self.body_markdown:
+ self.body_html = markdown_processor(self.body_markdown)
+
+ def read(self):
+ with open(self.filename, "r", encoding="utf-8") as f:
+ contents = f.read()
+ metayaml, self.body_markdown = contents.split('\n---')
+ return yaml.load(metayaml)
+
+class Page(_FileLoader):
+ pass
diff --git a/app/pages/templates/pages/about.html b/app/pages/templates/pages/about.html
new file mode 100644
index 0000000..a3eb9f9
--- /dev/null
+++ b/app/pages/templates/pages/about.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{% block pagetitle %}Cumulus Learning | {{object.title}}{% endblock %}
+{% block metadescription %}{{object.meta_description}}{% endblock %}
+{%block htmlclass%}class="detail"{%endblock%}
+{%block bodyid%}id="{{object.title|slugify}}"{%endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}<main>
+ <div class="post-article" style="margin-top: 4rem">
+ {{object.body_html|safe|smartypants|widont}}
+ </div>
+ </article>
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{%endif%}
+ </main>
+{% endblock %}
+{% block js %}
+<script src="/media/js/lightbox.js" type="text/javascript"></script>
+<script>var opts={captions:true,onload:function(){var im=document.getElementById("jslghtbx-contentwrapper");var link=im.appendChild(document.createElement('a'))
+link.href=im.firstChild.src;link.innerHTML="open ";link.target="_blank";link.setAttribute('class','p-link');im.appendChild(link);}};var lightbox=new Lightbox();lightbox.load(opts);</script>
+{% endblock %}
+
+
diff --git a/app/pages/templates/pages/homepage.html b/app/pages/templates/pages/homepage.html
new file mode 100644
index 0000000..537dd31
--- /dev/null
+++ b/app/pages/templates/pages/homepage.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'></title>
+ <link rel="canonical" href="https://tk.net/">{%endblock%}
+ {%block extrahead%}
+ {%endblock%}
+{%block bodyid%}id="home"{%endblock%}
+
+{% block primary %}
+<main>
+ <article class="content about">
+ <div class="big-top">
+ <h1>Hi, I'm Corrinne Gilbertson,</h1>
+ <p>a <a href="/what-is-structured-word-inquiry">literacy instructor</a>, <a href="">reading specialist</a>, &amp; <a href="">linguist</a>.
+ <p>I provide <a href="">group classes</a> and <a href="">private tutoring</a> to help<br />
+ struggling readers and spellers build a meaningful<br /> understanding of the English language.</p>
+ <p>Interested? Email me: <a href="mailto:corrinne@cumuluslearning.net">corrinne@cumuluslearning.net</a></p>
+ </div>
+ <div class="sign-form">
+ <h2>Sign Up To Learn More</h2>
+ <p>Drop your email in the box to be notified about group classes</p>
+ <p><iframe target='_parent' style="border:none !important; background:white; width:100% !important; height: 300px;" title="embedded form for class notifications" src="{% url 'lttr:subscribe' 'classes' %}"></iframe></p>
+ <p>Head over to the <a href="/classes/">class list</a> for more info.</p>
+ </div>
+ <div class="post-article" style="margin-top: 7rem">
+ {{object.body_html|safe|smartypants|widont}}
+ </div>
+ </article>
+ </main>
+{% endblock %}
diff --git a/app/pages/templates/pages/page_detail.html b/app/pages/templates/pages/page_detail.html
new file mode 100644
index 0000000..8b1741f
--- /dev/null
+++ b/app/pages/templates/pages/page_detail.html
@@ -0,0 +1,38 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{% block pagetitle %}Cumulus Learning | {{object.title}}{% endblock %}
+{% block metadescription %}{{object.meta_description}}{% endblock %}
+{%block htmlclass%}class="detail"{%endblock%}
+{%block bodyid%}id="{{object.title|slugify}}"{%endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+<main role="main" class="archive-wrapper">
+ <article>
+ <header class="archive-intro">
+ <h1 class="archive-hed">{{object.title}}</h1>
+ {%if object.sub_title %}<h2 class="post-subtitle">{{object.sub_title}}</h2>{%endif%}
+ </header>
+ <div class="post-article">
+ {{object.body_html|safe|smartypants|widont}}
+ </div>
+ </article>
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{%endif%}
+ </main>
+{% endblock %}
+{% block js %}
+<script src="/media/js/lightbox.js" type="text/javascript"></script>
+<script>var opts={captions:true,onload:function(){var im=document.getElementById("jslghtbx-contentwrapper");var link=im.appendChild(document.createElement('a'))
+link.href=im.firstChild.src;link.innerHTML="open ";link.target="_blank";link.setAttribute('class','p-link');im.appendChild(link);}};var lightbox=new Lightbox();lightbox.load(opts);</script>
+{% endblock %}
+
+
diff --git a/app/pages/templates/pages/page_detail.txt b/app/pages/templates/pages/page_detail.txt
new file mode 100644
index 0000000..547ce79
--- /dev/null
+++ b/app/pages/templates/pages/page_detail.txt
@@ -0,0 +1,8 @@
+{{object.title|safe}}
+{% for letter in object.title %}={%endfor%}
+
+ by Scott Gilbertson
+ <{{SITE_URL}}{{object.get_absolute_url}}>
+ {{object.pub_date|date:"l, d F Y"}}
+
+{{object.body_markdown|safe}}
diff --git a/app/pages/urls.py b/app/pages/urls.py
new file mode 100644
index 0000000..c40eee0
--- /dev/null
+++ b/app/pages/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "pages"
+
+urlpatterns = [
+ path(
+ r'',
+ views.PageDetailView.as_view(),
+ name="detail"
+ ),
+]
diff --git a/app/pages/views.py b/app/pages/views.py
new file mode 100644
index 0000000..7ea2439
--- /dev/null
+++ b/app/pages/views.py
@@ -0,0 +1,40 @@
+from utils.views import LuxDetailView
+from django.views.generic import DetailView
+
+from posts.models import Post, PostType
+from .models import Page, HomePage
+
+
+class PageDetailView(LuxDetailView):
+ model = Page
+
+ def get_template_names(self):
+ obj = self.get_object()
+ return ["pages/%s.html" % (obj.slug), 'pages/page_detail.html']
+
+
+class PageDetailTXTView(LuxDetailView):
+ model = Page
+ slug_field = "slug"
+
+ def get_template_names(self):
+ obj = self.get_object()
+ return 'pages/%s/page_detail.txt'% obj.site.name
+
+
+class HomePageList(DetailView):
+ """
+ Return a main entry and list of Entries in reverse chronological order
+ """
+ model = HomePage
+
+ def get_template_names(self):
+ return ["pages/%s.html" % self.template_name, 'pages/homepage.html']
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(HomePageList, self).get_context_data(**kwargs)
+ context['object_list'] = Post.objects.filter(post_type=PostType.HOMEPAGE).filter(status__exact=1).order_by('-pub_date')[1:9]
+ return context
+
+
diff --git a/app/posts/__init__.py b/app/posts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/posts/__init__.py
diff --git a/app/posts/admin.py b/app/posts/admin.py
new file mode 100644
index 0000000..af39162
--- /dev/null
+++ b/app/posts/admin.py
@@ -0,0 +1,77 @@
+from django.contrib import admin
+from django import forms
+from django.contrib.gis.admin import OSMGeoAdmin
+from django.contrib.contenttypes.admin import GenericStackedInline
+
+from utils.widgets import AdminImageWidget, LGEntryForm
+from .models import Post
+
+from utils.util import get_latlon
+
+
+@admin.register(Post)
+class PostAdmin(OSMGeoAdmin):
+ form = LGEntryForm
+
+ def render_change_form(self, request, context, *args, **kwargs):
+ #context['adminform'].form.fields['featured_image'].queryset = LuxImage.objects.all()[:200]
+ return super(PostAdmin, self).render_change_form(request, context, *args, **kwargs)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'thumbnail' or db_field.name == 'image':
+ field = forms.FileField(widget=AdminImageWidget)
+ elif db_field.name == 'meta_description':
+ field = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 75}))
+ field.required = False
+ else:
+ field = super(PostAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+ return field
+
+ list_display = ('title', 'site', 'post_type', 'pub_date', 'template_name', 'status',)
+ search_fields = ['title', 'body_markdown']
+ prepopulated_fields = {"slug": ('title',)}
+ list_filter = ('site', 'post_type', 'pub_date', 'enable_comments', 'status')
+ fieldsets = (
+ ('Entry', {
+ 'fields': (
+ ('title', 'short_title'),
+ 'subtitle',
+ 'body_markdown',
+ ('pub_date', 'status', 'post_type'),
+ ('slug', 'enable_comments',),
+ 'dek',
+ 'meta_description',
+ 'template_name',
+ ('featured_image','related'),
+ 'site'
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }
+ ),
+ ('Extras', {
+ 'fields': (
+ ('has_video', 'disclaimer',),
+ 'topics',
+ 'prologue_markdown',
+ 'epilogue_markdown',
+ 'originally_published_by',
+ 'originally_published_by_url',
+ ),
+ 'classes': (
+ 'collapse',
+ )
+ }),
+ )
+
+ class Media:
+ js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js')
+ css = {
+ "all": ("my_styles.css",)
+ }
+
+
+
diff --git a/app/posts/build.py b/app/posts/build.py
new file mode 100644
index 0000000..47a6efe
--- /dev/null
+++ b/app/posts/build.py
@@ -0,0 +1,110 @@
+from django.urls import reverse
+from django.apps import apps
+from builder.base import BuildNew
+from itertools import chain
+
+from django.conf import settings
+from .models import PostType
+
+
+class BuildSrc(BuildNew):
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(post_type=PostType.SRC).filter(status__exact=1).order_by('-pub_date')
+
+ def build(self):
+ self.build_list_view(
+ base_path=reverse("src:list"),
+ paginate_by=50
+ )
+ self.build_detail_view()
+
+
+class BuildGuide(BuildNew):
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(post_type__in=[PostType.FIELD_TEST, PostType.REVIEW]).filter(status__exact=1).order_by('-pub_date')
+
+ def build(self):
+ self.build_list_view(
+ base_path=reverse("guides:guide-base"),
+ paginate_by=50
+ )
+ self.build_detail_view()
+
+
+class BuildFieldNotes(BuildNew):
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(post_type=PostType.FIELD_NOTE).filter(status__exact=1).order_by('-pub_date')
+
+ def build(self):
+ self.build_detail_view()
+ self.build_list_view(
+ base_path=reverse("fieldnotes:list"),
+ paginate_by=24
+ )
+ self.build_year_view("fieldnotes:list_year")
+ self.build_month_view("fieldnotes:list_month")
+
+
+class BuildJrnl(BuildNew):
+ '''
+ Write jrnl to disk
+ '''
+ def get_model_queryset(self):
+ return self.model.objects.filter(post_type=PostType.JRNL).filter(status__exact=1).order_by('-pub_date')
+
+ def build(self):
+ self.build_list_view(
+ base_path=reverse("jrnl:list"),
+ paginate_by=24
+ )
+ self.build_year_view("jrnl:list_year")
+ self.build_month_view("jrnl:list_month")
+ self.build_detail_view()
+ self.build_location_view()
+ self.build_latest()
+
+ def build_arc(self):
+ self.build_list_view(
+ base_path=reverse("jrnl:list"),
+ paginate_by=24
+ )
+ self.build_year_view("jrnl:list_year")
+ self.build_month_view("jrnl:list_month")
+ self.build_location_view()
+
+ def build_location_view(self):
+ c = apps.get_model('locations', 'Country')
+ r = apps.get_model('locations', 'Region')
+ countries = c.objects.filter(visited=True)
+ regions = r.objects.all()
+ locations = list(chain(countries, regions))
+ for c in locations:
+ try:
+ qs = self.model.objects.filter(
+ status__exact=1,
+ post_type=PostType.JRNL,
+ location__state__country=c
+ )
+ except:
+ qs = self.model.objects.filter(
+ status__exact=1,
+ post_type=PostType.JRNL,
+ location__state__country__lux_region=c.id
+ )
+ print(c)
+ pages = self.get_pages(qs, 24)
+ for page in range(pages):
+ base_path = reverse("jrnl:list_country", kwargs={'slug': c.slug, 'page': page + 1})
+ response = self.client.get(base_path)
+ print(response.content)
+ if page == 0:
+ self.write_file(base_path, response.content)
+ else:
+ self.write_file(base_path, response.content)
+
+ def build_latest(self):
+ response = self.client.get('/jrnl/latest/')
+ self.write_file(reverse("jrnl:latest"), response.content)
diff --git a/app/posts/importer.py b/app/posts/importer.py
new file mode 100644
index 0000000..7ed4782
--- /dev/null
+++ b/app/posts/importer.py
@@ -0,0 +1,107 @@
+for e in essaysold:
+ if e.featured_image:
+ feat = e.featured_image
+ else:
+ feat = None
+ if e.meta_description:
+ meta = e.meta_description
+ else:
+ meta = "need meta"
+ new, created = Post.objects.get_or_create(
+ old_id=e.pk,
+ post_type=2,
+ title=e.title,
+ subtitle=e.sub_title,
+ dek=e.dek,
+ slug=e.slug,
+ prologue_markdown=e.preamble,
+ body_markdown=e.body_markdown,
+ pub_date=e.pub_date,
+ enable_comments=e.enable_comments,
+ status=e.status,
+ meta_description=meta,
+ originally_published_by=e.originally_published_by,
+ originally_published_by_url=e.originally_published_by_url,
+ featured_image=feat,
+ has_video=e.has_video,
+ epilogue_markdown=e.afterword,
+ )
+ print(created)
+
+
+
+# migrate jrnl to posts
+for e in Entry.objects.all():
+ if e.meta_description:
+ meta_description = e.meta_description
+ else:
+ meta_description = "needs"
+ if e.image:
+ old_image = e.image
+ else:
+ old_image = None
+ p, created = Post.objects.get_or_create(
+ old_id=e.pk,
+ title = e.title,
+ short_title = '',
+ subtitle = e.subtitle,
+ slug = e.slug,
+ body_markdown = e.body_markdown,
+ body_html = e.body_html,
+ dek = e.dek,
+ meta_description = meta_description,
+ pub_date = e.pub_date,
+ enable_comments = e.enable_comments,
+ status = e.status,
+ featured_image = e.featured_image,
+ post_type = PostType.JRNL,
+ template_name = e.template_name,
+ has_video = e.has_video,
+ point = e.point,
+ location = e.location,
+ old_image=old_image
+ )
+ for b in e.books.all():
+ c = Book.objects.get(
+ slug=b.slug,
+ title=b.title,
+ )
+ p.books.add(c)
+ for f in e.field_notes.all():
+ c = Post.objects.get(
+ slug=f.slug,
+ title=f.title,
+ )
+ p.field_notes.add(c)
+ p.save()
+
+#Then after they're all in there:
+ctype = ContentType.objects.get(app_label='posts',model='post')
+oldctype = ContentType.objects.get(app_label='jrnl',model='entry')
+for e in Entry.objects.all():
+ p = Post.objects.get(title=e.title,old_id=e.id)
+ if e.related:
+ for t in e.related.all():
+ if t.model_name == oldctype:
+ tp = ctype
+ else:
+ tp = t.model_name
+ c = RelatedPost.objects.get(
+ model_name=tp,
+ title=t.title,
+ slug=t.slug,
+ pub_date=t.pub_date
+ )
+ p.related.add(c)
+ p.save()
+
+# Then to port comments:
+ctype = ContentType.objects.get(app_label='posts',model='post')
+oldctype = ContentType.objects.get(app_label='jrnl',model='entry')
+for c in Comment.objects.filter(content_type=oldctype):
+ e = Entry.objects.get(pk=c.object_pk)
+ p = Post.objects.get(title=e.title,old_id=e.id)
+ c.object_pk = p.pk
+ c.content_type = ctype
+ print("%s --> %s" %(c.content_object,p))
+ c.save()
diff --git a/app/posts/migrations/0001_initial.py b/app/posts/migrations/0001_initial.py
new file mode 100644
index 0000000..fe0faa8
--- /dev/null
+++ b/app/posts/migrations/0001_initial.py
@@ -0,0 +1,56 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('taxonomy', '0002_auto_20211006_2025'),
+ ('normalize', '0002_alter_relatedpost_id'),
+ ('sites', '0002_alter_domain_unique'),
+ ('media', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Post',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('short_title', models.CharField(blank=True, max_length=200, null=True)),
+ ('subtitle', models.CharField(blank=True, max_length=200)),
+ ('slug', models.SlugField(unique_for_date='pub_date')),
+ ('prologue_markdown', models.TextField(blank=True, null=True)),
+ ('prologue_html', models.TextField(blank=True, null=True)),
+ ('body_markdown', models.TextField()),
+ ('body_html', models.TextField(blank=True)),
+ ('epilogue_markdown', models.TextField(blank=True, null=True)),
+ ('epilogue_html', models.TextField(blank=True, null=True)),
+ ('dek', models.TextField(blank=True, null=True)),
+ ('meta_description', models.CharField(blank=True, max_length=256)),
+ ('pub_date', models.DateTimeField(verbose_name='Date published')),
+ ('last_updated', models.DateTimeField(auto_now=True)),
+ ('enable_comments', models.BooleanField(default=False)),
+ ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Published')], default=0)),
+ ('post_type', models.IntegerField(choices=[(0, 'field test'), (1, 'review'), (2, 'essay'), (3, 'src'), (4, 'jrnl'), (5, 'field note')], default=4)),
+ ('template_name', models.IntegerField(choices=[(0, 'single'), (1, 'double'), (2, 'single-dark'), (3, 'double-dark'), (4, 'single-black'), (5, 'double-black')], default=0)),
+ ('has_video', models.BooleanField(blank=True, default=False)),
+ ('has_code', models.BooleanField(blank=True, default=False)),
+ ('disclaimer', models.BooleanField(blank=True, default=False)),
+ ('originally_published_by', models.CharField(blank=True, max_length=400, null=True)),
+ ('originally_published_by_url', models.CharField(blank=True, max_length=400, null=True)),
+ ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')),
+ ('related', models.ManyToManyField(blank=True, to='normalize.RelatedPost')),
+ ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+ ('topics', models.ManyToManyField(blank=True, to='taxonomy.Category')),
+ ],
+ options={
+ 'ordering': ('-pub_date',),
+ 'get_latest_by': 'pub_date',
+ },
+ ),
+ ]
diff --git a/app/posts/migrations/0002_alter_post_post_type.py b/app/posts/migrations/0002_alter_post_post_type.py
new file mode 100644
index 0000000..0502bf0
--- /dev/null
+++ b/app/posts/migrations/0002_alter_post_post_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2022-02-04 20:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('posts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='post',
+ name='post_type',
+ field=models.IntegerField(choices=[(0, 'Homepage'), (1, 'review'), (2, 'essay'), (3, 'src'), (4, 'jrnl'), (5, 'field note')], default=4),
+ ),
+ ]
diff --git a/app/posts/migrations/__init__.py b/app/posts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/posts/migrations/__init__.py
diff --git a/app/posts/models.py b/app/posts/models.py
new file mode 100644
index 0000000..040fe52
--- /dev/null
+++ b/app/posts/models.py
@@ -0,0 +1,275 @@
+import datetime
+import os
+
+from django.dispatch import receiver
+from django.contrib.gis.db import models
+from django.db.models.signals import post_save
+from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.urls import reverse
+from django.utils.functional import cached_property
+from django.apps import apps
+from django.conf import settings
+from django.contrib.sitemaps import Sitemap
+from django import forms
+
+import urllib.request
+import urllib.parse
+import urllib.error
+from django_gravatar.helpers import get_gravatar_url, has_gravatar, calculate_gravatar_hash
+from django_comments.signals import comment_was_posted
+from django_comments.models import Comment
+from django_comments.moderation import CommentModerator, moderator
+
+from taggit.managers import TaggableManager
+
+from normalize.models import RelatedPost
+from media.models import LuxImage, LuxImageSize
+#from fieldnotes.models import FieldNote
+from taxonomy.models import TaggedItems, Category
+from utils.util import render_images, render_products, parse_video, markdown_to_html, extract_main_image
+
+
+def get_upload_path(self, filename):
+ return "images/post-images/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+class PostType(models.IntegerChoices):
+ HOMEPAGE = 0, ('Homepage')
+ REVIEW = 1, ('review')
+ ESSAY = 2, ('essay')
+ SRC = 3, ('src')
+ JRNL = 4, ('jrnl')
+ FIELD_NOTE = 5, ('field note')
+
+
+class Post(models.Model):
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+ title = models.CharField(max_length=200)
+ short_title = models.CharField(max_length=200, blank=True, null=True)
+ subtitle = models.CharField(max_length=200, blank=True)
+ slug = models.SlugField(unique_for_date='pub_date')
+ prologue_markdown = models.TextField(blank=True, null=True)
+ prologue_html = models.TextField(blank=True, null=True)
+ body_markdown = models.TextField()
+ body_html = models.TextField(blank=True)
+ epilogue_markdown = models.TextField(blank=True, null=True)
+ epilogue_html = models.TextField(blank=True, null=True)
+ dek = models.TextField(null=True, blank=True)
+ meta_description = models.CharField(max_length=256, blank=True)
+ pub_date = models.DateTimeField('Date published')
+ last_updated = models.DateTimeField(auto_now=True)
+ enable_comments = models.BooleanField(default=False)
+ PUB_STATUS = (
+ (0, 'Draft'),
+ (1, 'Published'),
+ )
+ status = models.IntegerField(choices=PUB_STATUS, default=0)
+ featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True)
+ TEMPLATES = (
+ (0, 'single'),
+ (1, 'double'),
+ (2, 'single-dark'),
+ (3, 'double-dark'),
+ (4, 'single-black'),
+ (5, 'double-black'),
+ )
+ post_type = models.IntegerField(choices=PostType.choices, default=PostType.JRNL)
+ template_name = models.IntegerField(choices=TEMPLATES, default=0)
+ has_video = models.BooleanField(blank=True, default=False)
+ has_code = models.BooleanField(blank=True, default=False)
+ disclaimer = models.BooleanField(blank=True, default=False)
+ related = models.ManyToManyField(RelatedPost, blank=True)
+ topics = models.ManyToManyField(Category, blank=True)
+ originally_published_by = models.CharField(max_length=400, null=True, blank=True)
+ originally_published_by_url = models.CharField(max_length=400, null=True, blank=True)
+
+ class Meta:
+ ordering = ('-pub_date',)
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ if self.post_type == 0:
+ return reverse('guides:reviews:review-detail', kwargs={"slug": self.slug})
+ if self.post_type == 1:
+ return reverse('guides:reviews:review-detail', kwargs={"slug": self.slug})
+ if self.post_type == 2:
+ return reverse('essays:detail', kwargs={"slug": self.slug})
+ if self.post_type == 3:
+ return reverse('src:detail', kwargs={"slug": self.slug})
+ if self.post_type == 5:
+ return reverse('fieldnote:detail', kwargs={"year": self.pub_date.year, "month": self.pub_date.strftime("%m"), "slug": self.slug})
+ if self.post_type == PostType.JRNL:
+ return reverse('jrnl:detail', kwargs={"year": self.pub_date.year, "month": self.pub_date.strftime("%m"), "slug": self.slug})
+
+ def comment_period_open(self):
+ return self.enable_comments and datetime.datetime.today() - datetime.timedelta(30) <= self.pub_date
+
+ def get_featured_image_thumb(self):
+ return self.featured_image.get_image_by_size("tn")
+
+ def get_content_type(self):
+ return ContentType.objects.get(app_label="posts", model="post")
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date(status__exact=1,post_type=self.post_type)
+
+ @property
+ def get_previous_admin_url(self):
+ n = self.get_previous_by_pub_date()
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[n.id] )
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date(status__exact=1,post_type=self.post_type)
+
+ @property
+ def get_next_admin_url(self):
+ model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name)
+ try:
+ return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk] )
+ except model.DoesNotExist:
+ return ''
+
+ @property
+ def longitude(self):
+ '''Get the site's longitude.'''
+ if self.point:
+ return self.point.x
+
+ @property
+ def latitude(self):
+ '''Get the site's latitude.'''
+ if self.point:
+ return self.point.y
+
+ @property
+ def sitemap_priority(self):
+ if self.post_type in [2,4,5]:
+ return 1.0
+ else:
+ return 0.7
+
+ def get_image_url(self):
+ '''
+ for legacy jrnl posts without a featured_image
+ '''
+ try:
+ image_dir, img = self.old_image.url.split('post-images/')[1].split('/')
+ return '%spost-images/%s/%s' % (settings.IMAGES_URL, image_dir, img)
+ except ValueError:
+ pass
+
+ def save(self, *args, **kwargs):
+ created = self.pk is None
+ if not created:
+ md = render_images(self.body_markdown)
+ prods = render_products(md)
+ print(self.title)
+ self.body_html = markdown_to_html(prods)
+ if self.epilogue_html:
+ self.epilogue_html = markdown_to_html(self.epilogue_markdown)
+ if self.prologue_html:
+ self.prologue_html = markdown_to_html(self.prologue_markdown)
+ self.has_video = parse_video(self.body_html)
+ if created and not self.featured_image:
+ if self.post_type == PostType.FIELD_NOTE:
+ self.featured_image = extract_main_image(self.body_markdown)
+ else:
+ self.featured_image = LuxImage.objects.latest()
+ old = type(self).objects.get(pk=self.pk) if self.pk else None
+ if old and old.featured_image != self.featured_image: # Field has changed
+ if self.featured_image:
+ s = LuxImageSize.objects.get(name="featured_jrnl")
+ ss = LuxImageSize.objects.get(name="picwide-med")
+ self.featured_image.sizes.add(s)
+ self.featured_image.sizes.add(ss)
+ self.featured_image.save()
+ if old and old.title != self.title or old and old.slug != self.slug:
+ related, c = RelatedPost.objects.get_or_create(model_name=self.get_content_type(), entry_id = self.id, pub_date=self.pub_date)
+ related.title = self.title
+ related.slug = self.slug
+ related.save()
+ super(Post, self).save(*args, **kwargs)
+
+
+class PostModerator(CommentModerator):
+ '''
+ Moderate everything except people with multiple approvals
+ '''
+ email_notification = True
+
+ def moderate(self, comment, content_object, request):
+ previous_approvals = Comment.objects.filter(user_email=comment.email, is_public=True)
+ for approval in previous_approvals:
+ if approval.submit_date <= datetime.datetime.today() - datetime.timedelta(21):
+ approve = True
+ if previous_approvals.count() > 2 and approve:
+ return False
+ # do entry build right here so it goes to live site
+ return True
+moderator.register(Post, PostModerator)
+
+
+@receiver(comment_was_posted, sender=Comment)
+def cache_gravatar(sender, comment, **kwargs):
+ gravatar_exists = has_gravatar(comment.email)
+ grav_dir = settings.IMAGES_ROOT + '/gravcache/'
+ if gravatar_exists:
+ url = get_gravatar_url(comment.email, size=60)
+ if not os.path.isdir(grav_dir):
+ os.makedirs(grav_dir)
+ local_grav = '%s/%s.jpg' % (grav_dir, calculate_gravatar_hash(comment.email))
+ urllib.request.urlretrieve(url, local_grav)
+
+
+@receiver(post_save, sender=Post)
+def post_save_events(sender, update_fields, created, instance, **kwargs):
+ related, created = RelatedPost.objects.get_or_create(model_name=instance.get_content_type(), entry_id = instance.id, pub_date=instance.pub_date, title=instance.title, slug=instance.slug)
+ post_save.disconnect(post_save_events, sender=Post)
+ instance.save()
+ post_save.connect(post_save_events, sender=Post)
+
+
+class PostSitemap(Sitemap):
+ changefreq = "never"
+ protocol = "https"
+
+ def items(self):
+ return Post.objects.filter(status=1)
+
+ def lastmod(self, obj):
+ return obj.pub_date
+
+ def priority(self, obj):
+ return obj.sitemap_priority
+
+
+"""
+for p in src:
+ s, created = Post.objects.get_or_create(
+ old_id=p.id,
+ title=p.title,
+ slug=p.slug,
+ body_markdown=p.body_markdown,
+ pub_date=p.pub_date,
+ enable_comments=p.enable_comments,
+ has_code=p.has_code,
+ status=p.status,
+ meta_description=p.meta_description,
+ post_type=3,
+ )
+ print(p)
+ for t in p.topics.all():
+ c,created = Category.objects.get_or_create(
+ slug=t.slug,
+ name=t.name,
+ pluralized_name=t.pluralized_name
+ )
+ s.topics.add(c)
+"""
diff --git a/app/posts/templates/horizontal_select.html b/app/posts/templates/horizontal_select.html
new file mode 100644
index 0000000..61dcfd8
--- /dev/null
+++ b/app/posts/templates/horizontal_select.html
@@ -0,0 +1,17 @@
+{% with id=widget.attrs.id %}
+ <ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>
+ {% for group, options, index in widget.optgroups %}
+ {% if group %}
+ <li>{{ group }}
+ <ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>
+ {% endif %}
+ {% for option in options %}
+ <li data-imageid="{{option.value}}" data-loopcounter="{{forloop.parentloop.counter}}">{% include option.template_name with widget=option %}</li>
+ {% endfor %}
+ {% if group %}
+ </ul>
+ </li>
+ {% endif %}
+ {% endfor %}
+ </ul>
+{% endwith %}
diff --git a/app/posts/templates/posts/essay_detail.html b/app/posts/templates/posts/essay_detail.html
new file mode 100644
index 0000000..e95c161
--- /dev/null
+++ b/app/posts/templates/posts/essay_detail.html
@@ -0,0 +1,178 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{%block htmlclass%}class="detail single"{%endblock%}
+{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %}
+
+{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %}
+{%block extrahead%}
+{% if object.has_code %}
+ <link rel="stylesheet" href="/media/src/solarized.css" type="text/css" media="screen"/>
+{%endif %}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{self.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>{% if object.featured_image %}
+ <meta name="twitter:image:src" content="{{object.featured_image.get_image_url}}"/>{%endif%}
+ <meta name="twitter:creator" content="@luxagraf"/>
+{%endblock%}
+
+{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+ <main>
+ <article class="h-entry hentry {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/Article">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ <h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>
+ <div class="post-linewrapper">
+ {% if object.originally_published_by %}<h4 class="post-source">Originally Published By: <a href="{{object.originally_published_by_url}}" title="View {{object.title}} on {{object.originally_published_by}}">{{object.originally_published_by}}</a></h4>{%endif%}
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%} post-essay" itemprop="articleBody">
+ {% if object.prologue_html %}<div class="afterward">
+ {{object.prologue_html|smartypants|safe}}
+ </div>{%endif%}
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {% if object.afterword_html %}<div class="afterward">
+ <h4>Afterward</h4>
+ {{object.afterword_html|smartypants|safe}}
+ </div>{%endif%}
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+
+ {% comment %} <div class="mailing-list--wrapper">
+ <h5>If you enjoyed this, you should join the mailing&nbsp;list&hellip;</h5>
+ {% include 'mailing_list.html' %}
+ </div> {% endcomment %}
+ </main>
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{% else %}
+<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p>
+{%endif%}
+{% endblock %}
+{% block js %}
+<script type="text/javascript">
+document.addEventListener("DOMContentLoaded", function(event) {
+ var leaflet = document.createElement('script');
+ leaflet.src = "/media/js/leaflet-master/leaflet-mod.js";
+ document.body.appendChild(leaflet);
+ var lightbox = document.createElement('script');
+ lightbox.src = "/media/js/lightbox.js";
+ document.body.appendChild(lightbox);
+ leaflet.onload = function(){
+ var detail = document.createElement('script');
+ detail.src = "/media/js/detail.min.js";
+ document.body.appendChild(detail);
+ {% with object.get_template_name_display as t %}{%if t == "single" or t == "single-dark" %}
+ detail.onload = function(){
+ createMap();
+ var open = false;
+ }
+ {%endif%}{%endwith%}
+ }
+
+ lightbox.onload = function() {
+ var opts= {
+ //nextOnClick: false,
+ captions: true,
+ onload: function(){
+ var im = document.getElementById("jslghtbx-contentwrapper");
+ var link = im.appendChild(document.createElement('a'))
+ link.href = im.firstChild.src;
+ link.innerHTML= "open ";
+ link.target = "_blank";
+ link.setAttribute('class', 'p-link');
+ im.appendChild(link);
+ }
+ };
+ var lightbox = new Lightbox();
+ lightbox.load(opts);
+ }
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+ //delay loading of gravatar images using noscript data-hash attribute
+ dataattr = document.getElementsByClassName("datahashloader");
+ for(var i=0; i<dataattr.length; i++) {
+ var c = dataattr[i].parentNode;
+ var img = document.createElement("img");
+ img.src = 'https://images.luxagraf.net/gravcache/' + dataattr[i].getAttribute('data-hash') + '.jpg';
+ img.className += "gravatar";
+ c.insertBefore(img, c.childNodes[3]);
+ }
+{%endif%}
+{%endif%}
+{% if object.has_video %}
+var tester = document.getElementsByClassName("vidauto");
+var wrapper = document.getElementById('wrapper');
+var dist = 100;
+
+window.onscroll = function() {
+ for (var i=0; i<tester.length; i++) {
+ checkVisible(tester[i]) ? tester[i].play() : tester[i].pause();
+ }
+};
+
+function checkVisible(elm) {
+ var rect = elm.getBoundingClientRect();
+ var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
+ return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
+}
+{%endif%}
+
+});
+</script>
+{%endblock%}
diff --git a/app/posts/templates/posts/essay_list.html b/app/posts/templates/posts/essay_list.html
new file mode 100644
index 0000000..8a35225
--- /dev/null
+++ b/app/posts/templates/posts/essay_list.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+
+{% block pagetitle %}Collected Essays of Scott Gilbertson {% endblock %}
+{% block metadescription %}Collected writing: essays, articles and stories on travel, photography, tools, walking, the natural world and other ephemera.{% endblock %}
+{% block breadcrumbs %}{% if breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{%endif%}{% endblock %}
+{% block primary %}<main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Essays &amp; Articles</h2>
+ <p>Topics include travel, writing, photography, free software, culture, and once, Del Taco.</p>
+ <p>Some essays below were previously published in: <em><a href="https://www.wired.com/author/scott-gilbertson/" rel="me">WIRED</a></em>, <em><a href="https://www.budgettravel.com/article/0902_HTTN_SocialNetwork_5488">Budget Travel</a></em>, <em><a href="https://arstechnica.com/">Ars Technica</a></em>, <em><a href="https://www.epicurious.com/contributors/scott-gilbertson" rel="me">Epicurious</a></em>, <em><a href="https://web.archive.org/web/20100904114555/http://one.longshotmag.com/article/going-for-seconds">Longshot Magazine</a>,</em> <em><a href="https://web.archive.org/web/20150506051746/http://1888.center/scott-gilbertson/" rel="me">The Cost of Paper</a></em> and elsewhere.</a></p>
+ </div>
+ <h1 class="topic-hed">Essays</h1>
+ <ul>{% for object in object_list %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <span class="date dt-published">{{object.pub_date|date:"F Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ <p class="p-summary">{% if object.subtitle %}{{object.subtitle|safe|smartypants}}{%else%}{{object.meta_description}}{%endif%}</p>
+ </a>
+ </li>
+ {%endfor%}</ul>
+ </main>
+{%endblock%}
diff --git a/app/posts/templates/posts/fieldnote_archive_list_date.html b/app/posts/templates/posts/fieldnote_archive_list_date.html
new file mode 100644
index 0000000..5d6865f
--- /dev/null
+++ b/app/posts/templates/posts/fieldnote_archive_list_date.html
@@ -0,0 +1,43 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% block pagetitle %} Field Notes | luxagraf {% endblock %}
+{% block metadescription %} Rough notes and sketches from the field {% endblock %}
+{%block bodyid%}id="field-notes"{%endblock%}
+
+{% block primary %}
+ <ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li>{% if month or year %}<a href="{% url 'fieldnotes:list' %}">Field Notes</a> &rarr;{%else%}Field Notes{%endif%}</li>
+ <li>{% if not month %}{{year|date:"Y"}}{%else%}<a href="/field-notes/{{month|date:"Y"}}/">{{month|date:"Y"}}</a> &rarr;{%endif%}</li>
+ {% if month %}<li itemprop="title">{{month|date:"F"}}</li>{% endif %}
+ </ul>
+ <main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Field Notes {% if month or year %}{% if month %} from {{month|date:"F"}} {{month|date:"Y"}}{%else%} from {{year|date:"Y"}}{%endif%}{%endif%}</h2>
+ <p>Quick notes, sketches and images from the road. This is the semi-orgnized brain dump that comes before the more organized <a href="/jrnl/" title="read the journal">journal entries</a> and <a href="/essays/" title="read essays">essays</a>. If I used social media this is the stuff I'd probably put there, but I prefer to put it here, even if it means a lot few people read it.</p>
+ </div>
+ <ul class="fancy-archive-list">{% for object in object_list %}{% if object.slug != 'about' %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <a href="{{object.get_absolute_url}}">
+ {% if object.featured_image %}<div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" /></div>{%endif%}
+ <span class="date dt-published">{{object.pub_date|date:"F d, Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
+ {% if object.location %}<h4 class="p-location h-adr post-location" itemprop="geo" itemscope itemtype="http://data-vocabulary.org/Geo">
+ <span class="p-locality">{{object.location.name|smartypants|safe}}</span>,
+ <span class="p-region">{{object.location.state_name}}</span>,
+ <span class="p-country-name">{{object.location.country_name}}</span>
+ <data class="p-latitude" value="{{object.latitude}}"></data>
+ <data class="p-longitude" value="{{object.longitude}}"></data>
+ </h4>{% endif %}
+ </li>
+ {%endif%}{%endfor%}</ul>
+ </main>
+
+{% endblock %}
+
+
+
diff --git a/app/posts/templates/posts/fieldnote_detail.html b/app/posts/templates/posts/fieldnote_detail.html
new file mode 100644
index 0000000..3368c56
--- /dev/null
+++ b/app/posts/templates/posts/fieldnote_detail.html
@@ -0,0 +1,114 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% load month_number_to_name %}
+{% block pagetitle %}{{object.title|title|smartypants|safe}} - Luxagraf, Field Notes{% endblock %}
+
+{% block metadescription %}{{object.body_html|striptags|safe|truncatewords:30}}{% endblock %}
+{%block extrahead%}
+ <link rel="canonical" href="http://luxagraf.net{{object.get_absolute_url}}" />
+ <meta name="ICBM" content="{{object.latitude}}, {{object.longitude}}" />
+ <meta name="geo.position" content="{{object.latitude}}; {{object.longitude}}" />
+ <meta name="geo.placename" content="{% if object.location.country.name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.state.name}}{%else%}{{object.location.name|smartypants|safe}}, {{object.country.name}}{%endif%}">
+ <meta name="geo.region" content="{{object.country.iso2}}{%if object.state.code != '' %}-{{object.state.code}}{%endif%}">
+{%endblock%}
+{% block bodyid %}class="notes--permalin detail" id="archive-{% if month %}{{month|month_number_to_name}}{%endif%}{{year}}"{%endblock%}
+{% block breadcrumbs %}
+<ol class="bl" id="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <a itemprop="item" href="/"><span itemprop="name">Home</span></a> &rarr;
+ <meta itemprop="position" content="1" />
+ </li>
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <span itemprop="item">
+ <a href="/field-notes/" itemprop="item"><span itemprop="name">Field Notes</span></a>{%if object.short_title%} &rarr;{%endif%}
+ </span>
+ <meta itemprop="position" content="2" />
+ </li>
+ {%if object.short_title%}<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <span itemprop="item">
+ <span itemprop="item"><span itemprop="name">{{object.short_title}}</span></span>
+ </span>
+ <meta itemprop="position" content="3" />
+ </li>{%endif%}
+ </ol>
+{% endblock %}
+{% block primary %}<main role="main">
+ <article class="h-entry hentry {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/BlogPosting">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ {% if object.subtitle %}<h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>{%endif%}
+ <div class="post-linewrapper">
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div class="e-content">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ <span class="p-author h-card">
+ <data class="p-name" value="Scott Gilbertson"></data>
+ <data class="u-url" value="https://luxagraf.net/"></data>
+ </span>
+ <footer>
+ {%comment%}<p class="note--date">
+ <a class="u-url" href="{{object.get_absolute_url}}" rel="bookmark"><time class="dt-published" datetime="{{object.pub_date|html5_datetime}}">{{object.pub_date|date:"F j, Y"}}</time></a>
+ </p>{%endcomment%}
+ {% comment %} {% if object.twitter_id %}
+ <ul class="note--actions">
+ <li><a rel="syndication" class="u-syndication" href="https://twitter.com/luxagraf/status/{{object.twitter_id}}">View on Twitter</a></li>
+ <li>
+ <indie-action do="reply" with="{{SITE_URL}}{{object.get_absolute_url}}"><a href="https://twitter.com/intent/tweet?in_reply_to={{object.twitter_id}}">Reply</a></indie-action>
+ </li>
+ <li>
+ <indie-action do="post" with="{{SITE_URL}}{{object.get_absolute_url}}">
+ <a href="https://twitter.com/intent/retweet?tweet_id={{object.twitter_id}}">Retweet</a>
+ </indie-action>
+ </li>
+ <li>
+ <indie-action do="bookmark" with="{{SITE_URL}}{{object.get_absolute_url}}">
+ <a href="https://twitter.com/intent/favorite?tweet_id={{object.twitter_id}}">Favourite</a>
+ </indie-action>
+ </li>
+ </ul>{% endif %}{% endcomment %}
+ </footer>
+
+
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <nav id="page-navigation">
+ <ul>{% if prev%}
+ <li rel="previous" id="next"><span class="bl">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </li>{%endif%}{% if next%}
+ <li rel="next" id="prev"><span class="bl">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ </article>
+</main>
+{% endblock %}
+
+{% block js %}
+<script>
+document.addEventListener("DOMContentLoaded", function(event) {
+ var leaflet = document.createElement('script');
+ leaflet.src = "/media/js/leaflet-master/leaflet-mod.js";
+ document.body.appendChild(leaflet);
+ leaflet.onload = function(){
+ var detail = document.createElement('script');
+ detail.src = "/media/js/detail.min.js";
+ document.body.appendChild(detail);
+ detail.onload = function(){
+ createMap();
+ var open = false;
+ }
+ }
+});
+</script>
+{%endblock%}
diff --git a/app/posts/templates/posts/fieldnote_list.html b/app/posts/templates/posts/fieldnote_list.html
new file mode 100644
index 0000000..14d61c0
--- /dev/null
+++ b/app/posts/templates/posts/fieldnote_list.html
@@ -0,0 +1,54 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load get_next %}
+{% load html5_datetime %}
+{% load pagination_tags %}
+{% block pagetitle %} Field Notes | luxagraf {% endblock %}
+{% block metadescription %}Rough notes and sketches from the field {% endblock %}
+{%block bodyid%}id="field-notes"{%endblock%}
+{% block breadcrumbs %}
+<ol class="bl" id="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <a itemprop="item" href="/"><span itemprop="name">Home</span></a> &rarr;
+ <meta itemprop="position" content="1" />
+ </li>
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <span itemprop="item">
+ <span itemprop="name" class="faint">Field Notes</span>
+ </span>
+ <meta itemprop="position" content="2" />
+ </li>
+ </ol>
+{% endblock %}
+{% block primary %}<main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Field Notes</h2>
+ <p>Quick notes, sketches, and images from the road. This is the semi-organized brain dump that comes before the more organized <a href="/jrnl/" title="read the journal">journal entries</a>. If I used social media this is the stuff I'd probably put there, but I prefer to put it here, even if it means a lot fewer people read it.</p>
+ </div>
+ {% autopaginate object_list 24 %}
+ <ul class="fancy-archive-list">{% for object in object_list %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <a href="{{object.get_absolute_url}}" class="u-url">
+ {% if object.featured_image %}<div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" class="u-photo" /></div>{%endif%}
+ <span class="datei"></span><span class="date dt-published">{{object.pub_date|date:"F d, Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
+ {% if object.location %}<h4 class="p-location h-adr post-location" itemprop="geo" itemscope itemtype="http://data-vocabulary.org/Geo">
+ <span class="p-locality">{{object.location.name|smartypants|safe}}</span>,
+ <span class="p-region">{{object.location.state_name}}</span>,
+ <span class="p-country-name">{{object.location.country_name}}</span>
+ <data class="p-latitude" value="{{object.latitude}}"></data>
+ <data class="p-longitude" value="{{object.longitude}}"></data>
+ </h4>{% endif %}
+ </li>
+ {%endfor%}</ul>
+ </main>
+ <nav aria-label="page navigation" class="pagination">
+ {% paginate %}
+ </nav>
+{% endblock %}
+
+
+
diff --git a/app/posts/templates/posts/guide_base.html b/app/posts/templates/posts/guide_base.html
new file mode 100644
index 0000000..e7764db
--- /dev/null
+++ b/app/posts/templates/posts/guide_base.html
@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% load pagination_tags %}
+{% block pagetitle %}Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.{% endblock %}
+{% block metadescription %}Guides for fellow travelers: tools, tips, and tricks to make life on the road in an RV or Van easier and more enjoyable.{% endblock %}
+
+{% block primary %}<ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li itemprop="title">guide</li>
+ </ul>
+ <main role="main" id="guide-archive" class="essay-archive guide-archive archive-list">
+ <div class="essay-intro">
+ <h2>Roaming Guide</h2>
+ <h3>Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.</h3>
+ <p>I don't want to tell you how to travel. Everyone is different. Besides, even after twenty some odd years of travel, I am still learning. </p>
+ <p>I've always been most inspired by wandering monks and nuns, those who walked or sailed with next to nothing and survived. Mostly. Today most of us are not that skilled or strong of will, but keeping that example in mind is helpful. The less stuff you travel with the better off you are. Up to a point. Having the right tools is important. The right tools make life easier and more fun.</p>
+ <p>I put this together to help you find the tools you need. These aren't casual reviews. These are things I have spent years seeking out, using, and refining. In the end what you need are not things, but strategies and tools that allow you to create solutions to problems. </p>
+ </div>
+ <h1 class="topic-hed">Reviews</h1>
+ {% autopaginate object_list 30 %}
+ <ul class="fancy-archive-list">{% for object in object_list %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <a href="{{object.get_absolute_url}}" class="u-url">
+ <div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" class="u-photo" /></div>
+ <span class="date dt-published">{{object.pub_date|date:"F d, Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
+ {% if object.location %}<h4 class="p-location h-adr post-location" itemprop="geo" itemscope itemtype="http://data-vocabulary.org/Geo">
+ <span class="p-locality">{{object.location.name|smartypants|safe}}</span>,
+ <span class="p-region">{{object.location.state_name}}</span>,
+ <span class="p-country-name">{{object.location.country_name}}</span>
+ <data class="p-latitude" value="{{object.latitude}}"></data>
+ <data class="p-longitude" value="{{object.longitude}}"></data>
+ </h4>{% endif %}
+ </li>
+ {%endfor%}</ul>
+ </main>
+{%endblock%}
diff --git a/app/posts/templates/posts/guide_detail.html b/app/posts/templates/posts/guide_detail.html
new file mode 100644
index 0000000..7e5df74
--- /dev/null
+++ b/app/posts/templates/posts/guide_detail.html
@@ -0,0 +1,187 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{%block htmlclass%}class="detail single"{%endblock%}
+{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %}
+
+{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %}
+{%block extrahead%}
+{% if object.has_code %}
+ <link rel="stylesheet" href="/media/src/solarized.css" type="text/css" media="screen"/>
+{%endif %}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{self.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>{% if object.featured_image %}
+ <meta name="twitter:image:src" content="{{object.featured_image.get_image_url}}"/>{%endif%}
+ <meta name="twitter:creator" content="@luxagraf"/>
+{%endblock%}
+
+{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%}
+
+{% block primary %}
+ <main>
+ <ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li><a href="/field-tests/" title="Advice, Tools, Tips and Tricks for Full Time Van, RV, and School Bus Life." itemprop="url"> <span itemprop="title">Field-Tests</span></a> &rarr; </li>
+ <li itemprop="title">{{object.short_title|smartypants|safe}}</li>
+ </ul>
+ <article class="h-entry hentry {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/Article">
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by
+ <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a>
+ </span>
+ <meta itemprop="image" content="https://images.luxagraf.net/header.gif" /> <!--{{self.featured_image.url}}" />-->
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ <h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>
+ <div class="post-linewrapper">
+ {% if object.originally_published_by %}<h4 class="post-source">Originally Published By: <a href="{{object.originally_published_by_url}}" title="View {{object.title}} on {{object.originally_published_by}}">{{object.originally_published_by}}</a></h4>{%endif%}
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <h3 class="post-location">Filed Under: <a href="/guides/">Guides</a>, {% for topic in object.topics.all %}<a href="/guides/topic/{{topic.slug}}">{{topic}}</a>{%endfor%}</h3>
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">Last Updated: {{object.last_updated|date:"F"}} <span>{{object.last_updated|date:"j, Y"}}</span></time>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%} post-guide" itemprop="articleBody">
+ {% if object.preamble %}<div class="afterward">
+ {{object.preamble_html|smartypants|safe}}
+ </div>{%endif%}
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {% if object.afterword_html %}<div class="afterward">
+ <h4>Afterward</h4>
+ {{object.afterword_html|smartypants|safe}}
+ </div>{%endif%}
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+
+ {% comment %} <div class="mailing-list--wrapper">
+ <h5>If you enjoyed this, you should join the mailing&nbsp;list&hellip;</h5>
+ {% include 'mailing_list.html' %}
+ </div> {% endcomment %}
+ </main>
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{% else %}
+<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p>
+{%endif%}
+{% endblock %}
+{% block js %}
+<script type="text/javascript">
+document.addEventListener("DOMContentLoaded", function(event) {
+ var leaflet = document.createElement('script');
+ leaflet.src = "/media/js/leaflet-master/leaflet-mod.js";
+ document.body.appendChild(leaflet);
+ var lightbox = document.createElement('script');
+ lightbox.src = "/media/js/lightbox.js";
+ document.body.appendChild(lightbox);
+ leaflet.onload = function(){
+ var detail = document.createElement('script');
+ detail.src = "/media/js/detail.min.js";
+ document.body.appendChild(detail);
+ {% with object.get_template_name_display as t %}{%if t == "single" or t == "single-dark" %}
+ detail.onload = function(){
+ createMap();
+ var open = false;
+ }
+ {%endif%}{%endwith%}
+ }
+
+ lightbox.onload = function() {
+ var opts= {
+ //nextOnClick: false,
+ captions: true,
+ onload: function(){
+ var im = document.getElementById("jslghtbx-contentwrapper");
+ var link = im.appendChild(document.createElement('a'))
+ link.href = im.firstChild.src;
+ link.innerHTML= "open ";
+ link.target = "_blank";
+ link.setAttribute('class', 'p-link');
+ im.appendChild(link);
+ }
+ };
+ var lightbox = new Lightbox();
+ lightbox.load(opts);
+ }
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+ //delay loading of gravatar images using noscript data-hash attribute
+ dataattr = document.getElementsByClassName("datahashloader");
+ for(var i=0; i<dataattr.length; i++) {
+ var c = dataattr[i].parentNode;
+ var img = document.createElement("img");
+ img.src = 'https://images.luxagraf.net/gravcache/' + dataattr[i].getAttribute('data-hash') + '.jpg';
+ img.className += "gravatar";
+ c.insertBefore(img, c.childNodes[3]);
+ }
+{%endif%}
+{%endif%}
+{% if object.has_video %}
+var tester = document.getElementsByClassName("vidauto");
+var wrapper = document.getElementById('wrapper');
+var dist = 100;
+
+window.onscroll = function() {
+ for (var i=0; i<tester.length; i++) {
+ checkVisible(tester[i]) ? tester[i].play() : tester[i].pause();
+ }
+};
+
+function checkVisible(elm) {
+ var rect = elm.getBoundingClientRect();
+ var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
+ return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
+}
+{%endif%}
+
+});
+</script>
+{%endblock%}
diff --git a/app/posts/templates/posts/jrnl_date.html b/app/posts/templates/posts/jrnl_date.html
new file mode 100644
index 0000000..dba1c53
--- /dev/null
+++ b/app/posts/templates/posts/jrnl_date.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% block pagetitle %} Field Notes | luxagraf {% endblock %}
+{% block metadescription %} Rough notes and sketches from the field {% endblock %}
+{%block bodyid%}id="field-notes"{%endblock%}
+
+{% block primary %}
+ <ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li>{% if month or year %}<a href="{% url 'jrnl:list' %}">Jrnl</a> &rarr;{%else%}Field Notes{%endif%}</li>
+ <li>{% if not month %}{{year|date:"Y"}}{%else%}<a href="/jrnl/{{month|date:"Y"}}/">{{month|date:"Y"}}</a> &rarr;{%endif%}</li>
+ {% if month %}<li itemprop="title">{{month|date:"F"}}</li>{% endif %}
+ </ul>
+ <main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Journal {% if month or year %}{% if month %} from {{month|date:"F"}} {{month|date:"Y"}}{%else%} from {{year|date:"Y"}}{%endif%}{%endif%}</h2>
+ </div>
+ <ul class="fancy-archive-list">{% for object in object_list %}{% if object.slug != 'about' %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <a href="{{object.get_absolute_url}}">
+ {% if object.featured_image %}<div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" /></div>{%endif%}
+ <span class="date dt-published">{{object.pub_date|date:"F d, Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
+ {% if object.location %}<h4 class="p-location h-adr post-location" itemprop="geo" itemscope itemtype="http://data-vocabulary.org/Geo">
+ <span class="p-locality">{{object.location.name|smartypants|safe}}</span>,
+ <span class="p-region">{{object.location.state_name}}</span>,
+ <span class="p-country-name">{{object.location.country_name}}</span>
+ <data class="p-latitude" value="{{object.latitude}}"></data>
+ <data class="p-longitude" value="{{object.longitude}}"></data>
+ </h4>{% endif %}
+ </li>
+ {%endif%}{%endfor%}</ul>
+ </main>
+
+{% endblock %}
+
+
+
diff --git a/app/posts/templates/posts/jrnl_detail.html b/app/posts/templates/posts/jrnl_detail.html
new file mode 100644
index 0000000..8a75f25
--- /dev/null
+++ b/app/posts/templates/posts/jrnl_detail.html
@@ -0,0 +1,240 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+
+{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %}
+
+{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %}
+{%block extrahead%}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta name="ICBM" content="{{object.latitude}}, {{object.longitude}}" />
+ <meta name="geo.position" content="{{object.latitude}}; {{object.longitude}}" />
+ <meta name="geo.placename" content="{% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.state.name}}{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name}}{%endif%}">
+ <meta name="geo.region" content="{{object.country.iso2}}{%if object.state.code != '' %}-{{object.state.code}}{%endif%}">
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{object.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{{object.meta_description}}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>
+ <meta name="twitter:image:src" content="{{object.get_featured_image}}"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://luxagraf.net{{object.get_absolute_url}}"
+ },
+ "headline": "{{object.title}}",
+ "datePublished": "{{object.pub_date|date:'c'}}+04:00",
+ "dateModified": "{{object.pub_date|date:'c'}}+04:00",
+ "author": {
+ "@type": "Person",
+ "name": "Scott Gilbertson"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "Luxagraf",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://luxagraf.net/media/img/logo-white.jpg"
+ }
+ },
+ "description": "{{object.meta_description}}"
+}
+</script>
+{%endblock%}
+{%block htmlclass%}{% with object.template_name as t %}
+class="detail {%if t == 1 or t == 3 or t == 5 %}double{%else%}single{%endif%}{%if t == 2 or t == 3 %} dark{%endif%}{%if t == 4 or t == 5 %} black{%endif%}"{%endwith%}{%endblock%}
+
+ {% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}
+ <main>
+ <article class="h-entry hentry entry-content content{% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/BlogPosting">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ {% if object.subtitle %}<h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>{%endif%}
+ <div class="post-linewrapper">
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%}" itemprop="articleBody">
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <div class="nav-wrapper">
+ <nav id="page-navigation" {%if wildlife or object.field_notes.all or object.books.all %}{%else%}class="page-border-top"{%endif%}>
+ <ul>{% if prev%}
+ <li id="prev"><span class="bl">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </li>{%endif%}{% if next%}
+ <li id="next"><span class="bl">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ </div>
+ {% if object.related.all %}<div class="article-afterward related">
+ <div class="related-bottom">
+ <h6 class="hedtinycaps">You might also enjoy</h6>
+ <ul class="article-card-list">{% for object in related %}
+ <li class="article-card-mini"><a href="{{object.get_absolute_url}}" title="{{object.title}}">
+ <div class="post-image post-mini-image">
+ {% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %}
+ {% elif object.image %}
+ {% include "lib/img_archive.html" with image=object.image nolightbox=True %}
+ {% else %}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}
+ </div>
+ <h4 class="p-name entry-title post-title" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <p class="post-summary">
+ {% if object.location %}<span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%}
+ </span>{%endif%}
+ {% if object.location and object.model_name.model != 'page' %}&ndash;{%endif%}
+ {% if object.model_name.model != 'page' %}<time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>{%endif%}
+ </p>
+ </a>
+ </li>
+ {% endfor %}</ul>
+ </div>
+ </div>{%endif%}
+
+ {% comment %} <div class="mailing-list--wrapper">
+ <h5>If you enjoyed this, you should join the mailing&nbsp;list&hellip;</h5>
+ {% include 'mailing_list.html' %}
+ </div> {% endcomment %}
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{% else %}
+<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p>
+{%endif%}
+</main>
+{% endblock %}
+{% block js %}
+<script>
+document.addEventListener("DOMContentLoaded", function(event) {
+ var leaflet = document.createElement('script');
+ leaflet.src = "/media/js/leaflet-master/leaflet-mod.js";
+ document.body.appendChild(leaflet);
+ var lightbox = document.createElement('script');
+ lightbox.src = "/media/js/lightbox.js";
+ document.body.appendChild(lightbox);
+ leaflet.onload = function(){
+ var detail = document.createElement('script');
+ detail.src = "/media/js/detail.min.js";
+ document.body.appendChild(detail);
+ {% with object.get_template_name_display as t %}{%if t == "single" or t == "single-dark" %}
+ detail.onload = function(){
+ createMap();
+ var open = false;
+ }
+ {%endif%}{%endwith%}
+ }
+
+ lightbox.onload = function() {
+ var opts= {
+ //nextOnClick: false,
+ captions: true,
+ onload: function(){
+ var im = document.getElementById("jslghtbx-contentwrapper");
+ var link = im.appendChild(document.createElement('a'))
+ link.href = im.firstChild.src;
+ link.innerHTML= "open ";
+ link.target = "_blank";
+ link.setAttribute('class', 'p-link');
+ im.appendChild(link);
+ }
+ };
+ var lightbox = new Lightbox();
+ lightbox.load(opts);
+ }
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+ //delay loading of gravatar images using noscript data-hash attribute
+ dataattr = document.getElementsByClassName("datahashloader");
+ for(var i=0; i<dataattr.length; i++) {
+ var c = dataattr[i].parentNode;
+ var img = document.createElement("img");
+ img.src = 'https://images.luxagraf.net/gravcache/' + dataattr[i].getAttribute('data-hash') + '.jpg';
+ img.className += "gravatar";
+ img.alt = "gravatar icon";
+ c.insertBefore(img, c.childNodes[3]);
+ }
+{%endif%}
+{%endif%}
+{% if object.has_video %}
+var tester = document.getElementsByClassName("vidauto");
+var wrapper = document.getElementById('wrapper');
+var dist = 100;
+
+window.onscroll = function() {
+ for (var i=0; i<tester.length; i++) {
+ checkVisible(tester[i]) ? tester[i].play() : tester[i].pause();
+ }
+};
+
+function checkVisible(elm) {
+ var rect = elm.getBoundingClientRect();
+ var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
+ return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
+}
+{%endif%}
+
+});
+</script>
+{%endblock%}
diff --git a/app/posts/templates/posts/jrnl_detail.txt b/app/posts/templates/posts/jrnl_detail.txt
new file mode 100644
index 0000000..a608b88
--- /dev/null
+++ b/app/posts/templates/posts/jrnl_detail.txt
@@ -0,0 +1,8 @@
+{{object.title|safe}}
+{% for letter in object.title %}={%endfor%}
+
+ by Scott Gilbertson
+ <https://luxagraf.net{{object.get_absolute_url}}>
+ {{object.pub_date|date:"l, d F Y"}}
+
+{{object.body_markdown|safe}}
diff --git a/app/posts/templates/posts/jrnl_latest.html b/app/posts/templates/posts/jrnl_latest.html
new file mode 100644
index 0000000..03e3c56
--- /dev/null
+++ b/app/posts/templates/posts/jrnl_latest.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+{% block js %}
+<script>
+window.location="{{object.get_absolute_url}}";
+</script>
+{% endblock %}
diff --git a/app/posts/templates/posts/jrnl_list.html b/app/posts/templates/posts/jrnl_list.html
new file mode 100644
index 0000000..6eefe10
--- /dev/null
+++ b/app/posts/templates/posts/jrnl_list.html
@@ -0,0 +1,33 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load pagination_tags %}
+{% block pagetitle %}Luxagraf | {% if region %}Travel Writing from {{region.name|title|smartypants|safe}}{%else%}Travel Writing from Around the World {%endif%}{% if page != "1" %} -- Page {{page}}{%endif%}{% endblock %}
+{% block metadescription %}{% if region %}Travel writing, essays and dispatches from {{region.name|title|smartypants|safe}}{%else%}Travel writing, essays and dispatches from around the world{%endif%} Page {{page}}{% endblock %}
+{%block bodyid%}id="writing" class="archive"{%endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}<main class="archive-grid">
+ <h1 class="hide">{% if region %}Journal entries from {%if region.name == 'United States'%}the United States{%else%}{{region.name|title|smartypants|safe}}{%endif%}{%else%}Journal {%endif%}</h1>{% autopaginate object_list 24 %} {% for object in object_list %}
+ <article class="h-entry hentry archive-card {% cycle 'odd' 'even' %} {% cycle 'first' 'second' 'third' %}" itemscope itemType="http://schema.org/Article">
+ <div class="post-image">
+ <a href="{{object.get_absolute_url}}" title="{{object.title}}">{% if object.featured_image %}
+ {% include "lib/img_archive.html" with image=object.featured_image %}
+ {%else%}
+ <img src="{{object.get_image_url}}" alt="{{ object.title }}" class="u-photo post-image" itemprop="image" />{%endif%}</a>
+ </div>
+ <h2 class="p-name entry-title post-title" itemprop="headline"><a href="{{object.get_absolute_url}}" class="u-url" title="{%if object.title_keywords%}{{object.title_keywords}}{%else%}{{object.title}}{%endif%}">{{object.title|safe|smartypants|widont}}</a></h2>
+ <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p>
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <p class="post-summary">
+ <span class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ {% if object.location.country_name == "United States" %}<span class="p-locality locality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name}}</a>, <span class="p-country-name">U.S.</span>{%else%}<span class="p-region">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}">{{object.location.country_name}}</a>{%endif%}
+ </span> &ndash;
+ <span class="p-summary" itemprop="description">
+ {{object.dek|safe}}
+ </span>
+ </p>
+ </article> {% endfor %}
+ </main>
+ <nav aria-label="page navigation" class="pagination">
+ {% paginate %}
+ </nav>
+{% endblock %}
diff --git a/app/posts/templates/posts/post_detail.html b/app/posts/templates/posts/post_detail.html
new file mode 100644
index 0000000..9e82786
--- /dev/null
+++ b/app/posts/templates/posts/post_detail.html
@@ -0,0 +1,183 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{%block htmlclass%}class="detail single"{%endblock%}
+{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %}
+
+{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %}
+{%block extrahead%}
+{% if object.has_code %}
+ <link rel="stylesheet" href="/media/src/solarized.css" type="text/css" media="screen"/>
+{%endif %}
+ <link rel="canonical" href="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Scott Gilbertson" />
+ <meta property="og:site_name" content="Luxagraf" />
+ <meta property="og:image" content="{{self.get_featured_image}}" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:description" content="{% if object.meta_description %}{{object.meta_description}}{%else%}{{object.subtitle}}{%endif%}"/>
+ <meta name="twitter:title" content="{{object.title|safe}}"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:domain" content="luxagraf"/>{% if object.featured_image %}
+ <meta name="twitter:image:src" content="{{object.featured_image.get_image_url}}"/>{%endif%}
+ <meta name="twitter:creator" content="@luxagraf"/>
+{%endblock%}
+
+{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%}
+
+{% block primary %}
+ <main>
+ <ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li><a href="/field-tests/" title="Advice, Tools, Tips and Tricks for Full Time Van, RV, and School Bus Life." itemprop="url"> <span itemprop="title">{{object.get_post_type_display}}</span></a> &rarr; </li>
+ <li itemprop="title">{{object.short_title|smartypants|safe}}</li>
+ </ul>
+ <article class="h-entry hentry {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/Article">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post-title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}</h1>
+ <h2 class="post-subtitle">{{object.subtitle|smartypants|safe}}</h2>
+ <div class="post-linewrapper">
+ {% if object.originally_published_by %}<h4 class="post-source">Originally Published By: <a href="{{object.originally_published_by_url}}" title="View {{object.title}} on {{object.originally_published_by}}">{{object.originally_published_by}}</a></h4>{%endif%}
+ {% if object.location %}<div class="p-location h-adr adr post-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place">
+ <h3 class="h-adr" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">{% if object.location.country_name == "United States" %}<span class="p-locality locality" itemprop="addressLocality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/jrnl/united-states/" title="travel writing from the United States">{{object.location.state_name|safe}}</a>, <span class="p-country-name" itemprop="addressCountry">U.S.</span>{%else%}<span class="p-region" itemprop="addressRegion">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.country_slug}}/" title="travel writing from {{object.location.country_name}}"><span itemprop="addressCountry">{{object.location.country_name|safe}}</span></a>{%endif%}</h3>
+ &ndash;&nbsp;<a href="" onclick="showMap({{object.latitude}}, {{object.longitude}}, { type:'point', lat:'{{object.latitude}}', lon:'{{object.longitude}}'}); return false;" title="see a map">Map</a>
+ </div>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+ <div id="article" class="e-content entry-content post--body post--body--{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%} post-essay" itemprop="articleBody">
+ {% if object.preamble %}<div class="afterward">
+ {{object.preamble_html|smartypants|safe}}
+ </div>{%endif%}
+ {{object.body_html|safe|smartypants}}
+ </div>
+ {% if object.afterword_html %}<div class="afterward">
+ <h4>Afterward</h4>
+ {{object.afterword_html|smartypants|safe}}
+ </div>{%endif%}
+ {%if wildlife or object.field_notes.all or object.books.all %}<div class="entry-footer">{%if wildlife %}
+ <aside id="wildlife">
+ <h3>Fauna and Flora</h3>
+ {% regroup wildlife by ap.apclass.get_kind_display as wildlife_list %}
+ <ul>
+ {% for object_list in wildlife_list %}
+ <li class="grouper">{{object_list.grouper}}<ul>
+ {% for object in object_list.list %}
+ <li>{%if object.ap.body_markdown%}<a href="{% url 'sightings:detail' object.ap.slug %}">{{object}}</a>{%else%}{{object}}{%endif%} </li>
+ {% endfor %}</ul>
+ {% endfor %}</ul>
+ </aside>
+ {% endif %}{%if object.field_notes.all %}
+ <aside {% if wildlife %}class="margin-left-none" {%endif%}id="field_notes">
+ <h3>Field Notes</h3>
+ <ul>{% for obj in object.field_notes.all %}
+ <li><a href="{% url 'fieldnotes:detail' year=obj.pub_date.year month=obj.pub_date|date:"m" slug=obj.slug %}">{{obj}}</a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ {%if object.books.all %}
+ <aside id="recommended-reading" {%if object.field_notes.all and wildlife %}class="rr-clear{%endif%}" >
+ <h3>Recommended Reading</h3>
+ <ul>{% for obj in object.books.all %}
+ <li><a href="{% url 'books:detail' slug=obj.slug %}"><img src="{{obj.get_small_image_url}}" /></a></li>
+ {% endfor %}</ul>
+ </aside>{% endif %}
+ </div>{%endif%}
+ </article>
+
+ {% comment %} <div class="mailing-list--wrapper">
+ <h5>If you enjoyed this, you should join the mailing&nbsp;list&hellip;</h5>
+ {% include 'mailing_list.html' %}
+ </div> {% endcomment %}
+ </main>
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{% else %}
+<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p>
+{%endif%}
+{% endblock %}
+{% block js %}
+<script type="text/javascript">
+document.addEventListener("DOMContentLoaded", function(event) {
+ var leaflet = document.createElement('script');
+ leaflet.src = "/media/js/leaflet-master/leaflet-mod.js";
+ document.body.appendChild(leaflet);
+ var lightbox = document.createElement('script');
+ lightbox.src = "/media/js/lightbox.js";
+ document.body.appendChild(lightbox);
+ leaflet.onload = function(){
+ var detail = document.createElement('script');
+ detail.src = "/media/js/detail.min.js";
+ document.body.appendChild(detail);
+ {% with object.get_template_name_display as t %}{%if t == "single" or t == "single-dark" %}
+ detail.onload = function(){
+ createMap();
+ var open = false;
+ }
+ {%endif%}{%endwith%}
+ }
+
+ lightbox.onload = function() {
+ var opts= {
+ //nextOnClick: false,
+ captions: true,
+ onload: function(){
+ var im = document.getElementById("jslghtbx-contentwrapper");
+ var link = im.appendChild(document.createElement('a'))
+ link.href = im.firstChild.src;
+ link.innerHTML= "open ";
+ link.target = "_blank";
+ link.setAttribute('class', 'p-link');
+ im.appendChild(link);
+ }
+ };
+ var lightbox = new Lightbox();
+ lightbox.load(opts);
+ }
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+ //delay loading of gravatar images using noscript data-hash attribute
+ dataattr = document.getElementsByClassName("datahashloader");
+ for(var i=0; i<dataattr.length; i++) {
+ var c = dataattr[i].parentNode;
+ var img = document.createElement("img");
+ img.src = 'https://images.luxagraf.net/gravcache/' + dataattr[i].getAttribute('data-hash') + '.jpg';
+ img.className += "gravatar";
+ c.insertBefore(img, c.childNodes[3]);
+ }
+{%endif%}
+{%endif%}
+{% if object.has_video %}
+var tester = document.getElementsByClassName("vidauto");
+var wrapper = document.getElementById('wrapper');
+var dist = 100;
+
+window.onscroll = function() {
+ for (var i=0; i<tester.length; i++) {
+ checkVisible(tester[i]) ? tester[i].play() : tester[i].pause();
+ }
+};
+
+function checkVisible(elm) {
+ var rect = elm.getBoundingClientRect();
+ var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
+ return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
+}
+{%endif%}
+
+});
+</script>
+{%endblock%}
diff --git a/app/posts/templates/posts/post_detail.txt b/app/posts/templates/posts/post_detail.txt
new file mode 100644
index 0000000..547ce79
--- /dev/null
+++ b/app/posts/templates/posts/post_detail.txt
@@ -0,0 +1,8 @@
+{{object.title|safe}}
+{% for letter in object.title %}={%endfor%}
+
+ by Scott Gilbertson
+ <{{SITE_URL}}{{object.get_absolute_url}}>
+ {{object.pub_date|date:"l, d F Y"}}
+
+{{object.body_markdown|safe}}
diff --git a/app/posts/templates/posts/post_list.html b/app/posts/templates/posts/post_list.html
new file mode 100644
index 0000000..9b3a1f3
--- /dev/null
+++ b/app/posts/templates/posts/post_list.html
@@ -0,0 +1,40 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load html5_datetime %}
+{% load pagination_tags %}
+{% block pagetitle %}Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.{% endblock %}
+{% block metadescription %}Guides for fellow travelers: tools, tips, and tricks to make life on the road in an RV or Van easier and more enjoyable.{% endblock %}
+
+{% block primary %}<ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
+ <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> &rarr; </li>
+ <li itemprop="title">{{archive_type}}</li>
+ </ul>
+ <main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Field Tests</h2>
+ <h3>Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.</h3>
+ <p>After {{years_on_the_road}} years on the road, here's what I know: all you really need is a vehicle, a stove and a cooler. After that, it's all luxury. The less stuff you travel with the better off you are. Up to a point. Having the right tools is important. The right tools make life easier and more fun.</p>
+ <p>I put this together to help you find the tools you need. These aren't casual reviews. These are things I have spent years seeking out, using, and refining. In the end what you need are not things, but solutions to problems. </p>
+ </div>
+ <h1 class="topic-hed">Reviews</h1>
+ {% autopaginate object_list 30 %}
+ <ul class="fancy-archive-list">{% for object in object_list %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <a href="{{object.get_absolute_url}}" class="u-url">
+ <div class="circle-img-wrapper"><img src="{{object.featured_image.get_thumbnail_url}}" alt="{{object.featured_image.alt}}" class="u-photo" /></div>
+ <span class="date dt-published">{{object.pub_date|date:"F d, Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ {% if object.subtitle %}<h3 class="p-summary">{{object.subtitle|safe|smartypants|widont}}</h3>{%endif%}
+ </a>
+ {% if object.location %}<h4 class="p-location h-adr post-location" itemprop="geo" itemscope itemtype="http://data-vocabulary.org/Geo">
+ <span class="p-locality">{{object.location.name|smartypants|safe}}</span>,
+ <span class="p-region">{{object.location.state_name}}</span>,
+ <span class="p-country-name">{{object.location.country_name}}</span>
+ <data class="p-latitude" value="{{object.latitude}}"></data>
+ <data class="p-longitude" value="{{object.longitude}}"></data>
+ </h4>{% endif %}
+ </li>
+ {%endfor%}</ul>
+ </main>
+{%endblock%}
diff --git a/app/posts/templates/posts/src_detail.html b/app/posts/templates/posts/src_detail.html
new file mode 100644
index 0000000..664db36
--- /dev/null
+++ b/app/posts/templates/posts/src_detail.html
@@ -0,0 +1,110 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+{% block pagetitle %}{{object.title|striptags}} - by Scott Gilbertson{% endblock %}
+{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %}
+{%block extrahead%}
+ <meta property="og:type" content="article" />
+ <meta property="og:site_name" content="luxagraf:src"/>
+ <meta property="og:title" content="{{object.title|safe}}" />
+ <meta property="og:url" content="https://luxagraf.net{{object.get_absolute_url}}" />
+ <meta property="og:image" content="">
+ <meta property="og:description" content="{{object.meta_description}}" />
+ <meta property="article:published_time" content="{{object.pub_date|date:'c'}}" />
+ <meta property="article:author" content="Luxagraf" />
+ <meta property="og:site_name" content="Luxagraf:src" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image"/>
+ <meta name="twitter:site" content="@luxagraf"/>
+ <meta name="twitter:creator" content="@luxagraf"/>
+ <link rel="stylesheet" href="/media/src/solarized.css" type="text/css" media="screen"/>
+{%endblock%}
+
+{% block bodyid %}class="src detail single"{% endblock %}
+{%block sitesubtitle %}Code Slowly{% endblock%}
+{% block breadcrumbs %}<ol class="bl" id="breadcrumbs" itemscope itemtype="http://schema.org/BreadcrumbList">
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <a itemprop="item" href="/">
+ <span itemprop="name">Home</span>
+ </a> &rarr;
+ <meta itemprop="position" content="1" />
+ </li>
+ <li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
+ <a href="/src/" itemprop="item">
+ <span itemprop="name">Src</span>
+ </a>
+ <meta itemprop="position" content="2" />
+ </li>
+ </ol>{% endblock %}
+{% block primary %}<main role="main">
+ <article class="hentry post-article{% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %} post--article--double{%endif%}{%endwith%}" itemscope itemType="http://schema.org/Article">
+ <header id="header" class="post-header {% with object.get_template_name_display as t %}{%if t == "double" or t == "double-dark" %}post--header--double{%endif%}{%endwith%}">
+ <h1 class="p-name entry-title post--title" itemprop="headline">{%if object.template_name == 1 or object.template_name == 3 %}{{object.title|safe|smartypants}}{%else%}{{object.title|safe|smartypants|widont}}{%endif%}</h1>
+ <h2 class="post-subtitle">{% if object.subtitle %}{{object.subtitle|smartypants|safe}}{%else%}{{object.meta_description|safe|smartypants|widont}}{%endif%}</h2>
+ <div class="post-linewrapper">
+ {% if object.originally_published_by %}<h4 class="post-source">Originally Published By: <a href="{{object.originally_published_by_url}}" title="View {{object.title}} on {{object.originally_published_by}}">{{object.originally_published_by}}</a></h4>{%endif%}
+ <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time>
+ <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span>
+ </div>
+ </header>
+
+
+ <div id="article" class="e-content entry-content post-body post-body-{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%}" itemprop="articleBody">
+ {{object.body_html|safe|smartypants|widont}}
+ </div>
+ </article>
+ {% if object.slug != 'about' %}
+ {% with object.get_next_published as next %}
+ {% with object.get_previous_published as prev %}
+ <nav id="page-navigation">
+ <ul>{% if prev%}
+ <li id="prev"><span class="bl">Previous:</span>
+ <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a>
+ </li>{%endif%}{% if next%}
+ <li id="next"><span class="bl">Next:</span>
+ <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a>
+ </li>{%endif%}
+ </ul>
+ </nav>{%endwith%}{%endwith%}
+ {%endif%}
+ </main>
+ {% if object.slug != 'about' %}
+ {% if object.enable_comments %}
+{% get_comment_count for object as comment_count %}
+{%if comment_count > 0 %}
+<p class="comments--header">{{comment_count}} Comment{{ comment_count|pluralize }}</p>
+{% render_comment_list for object %}
+{%endif%}
+<div class="comment--form--wrapper {%if comment_count > 0%}comment-form-border{%endif%}">
+{% render_comment_form for object %}
+</div>
+{% else %}
+<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p>
+{%endif%}
+{%endif%}
+{% endblock %}
+{% block js %}
+<script type="text/javascript">
+window.onload = function() {
+ {% if object.enable_comments %}
+{%if comment_count > 0 %}
+ //delay loading of gravatar images using noscript data-hash attribute
+ dataattr = document.getElementsByClassName("datahashloader");
+ for(var i=0; i<dataattr.length; i++) {
+ var c = dataattr[i].parentNode;
+ var img = document.createElement("img");
+ img.src = 'https://images.luxagraf.net/gravcache/' + dataattr[i].getAttribute('data-hash') + '.jpg';
+ img.className += "gravatar";
+ c.insertBefore(img, c.childNodes[3]);
+ }
+{%endif%}
+{%endif%}
+ {% with object.get_template_name_display as t %}{%if t == "single" or t == "single-dark" %}
+ createMap();
+ var open = false;
+ {%endif%}{%endwith%}
+}
+</script>
+{% if object.has_code %}
+{%endif %}
+{% endblock %}
diff --git a/app/posts/templates/posts/src_list.html b/app/posts/templates/posts/src_list.html
new file mode 100644
index 0000000..21a8c4e
--- /dev/null
+++ b/app/posts/templates/posts/src_list.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% load comments %}
+
+{% block pagetitle %}Tutorials and tools for building great things{% endblock %}
+{% block metadescription %}Tutorials and tools for building great things on the web - by Scott Gilbertson.{% endblock %}
+{%block sitesubtitle %}Code Slowly{% endblock%}
+{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %}
+{% block primary %}<main role="main" id="essay-archive" class="essay-archive archive-list">
+ <div class="essay-intro">
+ <h2>Tutorials and tools for building great things on the web.</h2>
+ <p>The indie web is an amazing democratic publishing platform unlike anything in history. The catch is, to avoid serving at the pleasure of the corporate king, you need to know <em>how</em> to publish. That's what these articles are here for, to help you learn how to use independent, community supported open source tools. The web won't last forever, let's build something cool while we can.</p>
+ <p>Topics include HTML, CSS, Django, Linux, Nginx, Python, Postgresql, free software, and, once, the evil that is Google AMP.</p>
+ <p>A few of the articles below were previously published in: <em><a href="https://arstechnica.com/">Ars Technica</a></em>, <em><a href="https://www.wired.com/author/scott-gilbertson/">Wired</a></em>, and <em><a href="https://www.theregister.co.uk/Author/Scott-Gilbertson/">The Register</a></em></p>
+ </div>
+ <h1 class="topic-hed">Articles</h1>
+ <ul class="fancy-archive-list">{% for object in object_list %}{% if object.slug != 'about' %}
+ <li class="h-entry hentry" itemscope itemType="http://schema.org/Article">
+ <span class="date dt-published">{{object.pub_date|date:"F Y"}}</span>
+ <a href="{{object.get_absolute_url}}">
+ <h2>{{object.title|safe|smartypants|widont}}</h2>
+ <p class="p-summary">{% if object.subtitle %}{{object.subtitle}}{%else%}{{object.meta_description|safe|smartypants|widont}}{%endif%}</p>
+ </a>
+ </li>
+ {%endif%}{%endfor%}</ul>
+
+
+
+ </main>
+{%endblock%}
diff --git a/app/posts/urls/__init__old.py b/app/posts/urls/__init__old.py
new file mode 100644
index 0000000..4993c34
--- /dev/null
+++ b/app/posts/urls/__init__old.py
@@ -0,0 +1,4 @@
+from .guide_urls import urlpatterns as guide_urlpatterns
+from .reviews_urls import urlpatterns as review_urlpatterns
+
+urlpatterns = review_urlpatterns + guide_urlpatterns
diff --git a/app/posts/urls/essay_urls.py b/app/posts/urls/essay_urls.py
new file mode 100644
index 0000000..3f0d7d7
--- /dev/null
+++ b/app/posts/urls/essay_urls.py
@@ -0,0 +1,29 @@
+from django.urls import path, re_path
+
+from ..views import guide_views as views
+
+app_name = "essays"
+
+urlpatterns = [
+ path(
+ r'<str:slug>',
+ views.PostDetailView.as_view(),
+ name="detail"
+ ),
+ path(
+ r'<str:slug>.txt',
+ views.PostDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ path(
+ r'<int:page>/',
+ views.EssayListView.as_view(),
+ name="list"
+ ),
+ path(
+ r'',
+ views.EssayListView.as_view(),
+ {'page':1},
+ name="list"
+ ),
+]
diff --git a/app/posts/urls/field_note_urls.py b/app/posts/urls/field_note_urls.py
new file mode 100644
index 0000000..7352b0e
--- /dev/null
+++ b/app/posts/urls/field_note_urls.py
@@ -0,0 +1,39 @@
+from django.urls import path, re_path
+
+from ..views import field_note_views as views
+
+app_name = "fieldnotes"
+
+urlpatterns = [
+ re_path(
+ r'(?P<year>[0-9]{4})/$',
+ views.FieldNoteYearArchiveView.as_view(),
+ name="list_year"
+ ),
+ path(
+ r'',
+ views.FieldNoteListView.as_view(),
+ {'page': 1},
+ name="list"
+ ),
+ path(
+ r'<page>/',
+ views.FieldNoteListView.as_view(),
+ name="list"
+ ),
+ path(
+ r'<int:year>/<int:month>/<str:slug>.txt',
+ views.FieldNoteDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ path(
+ r'<int:year>/<int:month>/<str:slug>',
+ views.FieldNoteDetailView.as_view(),
+ name="detail"
+ ),
+ path(
+ r'<int:year>/<int:month>/',
+ views.FieldNoteMonthArchiveView.as_view(month_format='%m'),
+ name="list_month"
+ ),
+]
diff --git a/app/posts/urls/guide_urls.py b/app/posts/urls/guide_urls.py
new file mode 100644
index 0000000..e0a2210
--- /dev/null
+++ b/app/posts/urls/guide_urls.py
@@ -0,0 +1,30 @@
+from django.urls import path, re_path, include
+from django.views.generic.base import RedirectView
+
+from ..views import guide_views as views
+
+app_name = "guides"
+
+urlpatterns = [
+ path(
+ r'',
+ views.GuideListView.as_view(),
+ {'page':1},
+ name="guide-base"
+ ),
+ path(r'field-test/', include('posts.urls.review_urls', namespace='reviews')),
+ re_path(r'^field-test/$', RedirectView.as_view(url='/field-tests/')),
+ #path(r'field-tests/', include('posts.urls', namespace='review-list')),
+ #path(r'review/', include('posts.review_urls')),
+ re_path(r'^review/$', RedirectView.as_view(url='/guides/')),
+ #path(
+ # r'<str:slug>',
+ # views.EntryDetailView.as_view(),
+ # name="detail"
+ #),
+ #re_path(
+ # r'<int:page>/',
+ # views.EntryList.as_view(),
+ # name="list"
+ #),
+]
diff --git a/app/posts/urls/guide_urls_old.py b/app/posts/urls/guide_urls_old.py
new file mode 100644
index 0000000..d43d76a
--- /dev/null
+++ b/app/posts/urls/guide_urls_old.py
@@ -0,0 +1,29 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "guides"
+
+urlpatterns = [
+ path(
+ r'<str:slug>',
+ views.PostDetailView.as_view(),
+ name="detail"
+ ),
+ path(
+ r'<str:slug>.txt',
+ views.PostDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ path(
+ r'<int:page>/',
+ views.GuidesListView.as_view(),
+ name="list"
+ ),
+ path(
+ r'',
+ views.GuidesListView.as_view(),
+ {'page':1},
+ name="list"
+ ),
+]
diff --git a/app/posts/urls/jrnl_urls.py b/app/posts/urls/jrnl_urls.py
new file mode 100644
index 0000000..d7f0fb1
--- /dev/null
+++ b/app/posts/urls/jrnl_urls.py
@@ -0,0 +1,60 @@
+from django.urls import path, re_path
+
+from ..views import jrnl_views as views
+
+app_name = "jrnl"
+
+urlpatterns = [
+ path(
+ r'feed.xml',
+ views.JrnlRSSFeedView(),
+ name="feed"
+ ),
+ re_path(
+ r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+).txt$',
+ views.JrnlDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ re_path(
+ r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$',
+ views.JrnlDetailView.as_view(),
+ name="detail"
+ ),
+ re_path(
+ r'^(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$',
+ views.JrnlMonthArchiveView.as_view(month_format='%m'),
+ name="list_month"
+ ),
+ re_path(
+ r'(?P<year>\d{4})/$',
+ views.JrnlYearArchiveView.as_view(),
+ name="list_year"
+ ),
+ re_path(
+ r'^(?P<page>\d+)/$',
+ views.JrnlListView.as_view(),
+ name="list"
+ ),
+ path(
+ r'latest/',
+ views.JrnlLatestView.as_view(),
+ name="latest"
+ ),
+ re_path(
+ r'(?P<slug>[-\w]+)/(?P<page>\d+)/$',
+ views.JrnlCountryListView.as_view(),
+ name="list_country"
+ ),
+ re_path(
+ r'^(?P<slug>[-\w]+)/$',
+ views.JrnlCountryListView.as_view(),
+ {'page':1},
+ name="list_country"
+ ),
+ re_path(
+ r'',
+ views.JrnlListView.as_view(),
+ {'page':1},
+ name="list"
+ ),
+]
diff --git a/app/posts/urls/review_urls.py b/app/posts/urls/review_urls.py
new file mode 100644
index 0000000..6b78e43
--- /dev/null
+++ b/app/posts/urls/review_urls.py
@@ -0,0 +1,29 @@
+from django.urls import path, re_path
+
+from ..views import guide_views as views
+
+app_name = "reviews"
+
+urlpatterns = [
+ path(
+ r'<str:slug>',
+ views.PostDetailView.as_view(),
+ name="review-detail"
+ ),
+ path(
+ r'<str:slug>.txt',
+ views.PostDetailViewTXT.as_view(),
+ name="review-detail-txt"
+ ),
+ path(
+ r'<int:page>/',
+ views.ReviewsListView.as_view(),
+ name="review-list"
+ ),
+ path(
+ r'',
+ views.ReviewsListView.as_view(),
+ {'page':1},
+ name="review-list"
+ ),
+]
diff --git a/app/posts/urls/src_urls.py b/app/posts/urls/src_urls.py
new file mode 100644
index 0000000..637d9a6
--- /dev/null
+++ b/app/posts/urls/src_urls.py
@@ -0,0 +1,49 @@
+from django.urls import path, re_path
+
+from ..views import src_views as views
+
+app_name = "src"
+
+urlpatterns = [
+ path(
+ r'feed.xml',
+ views.SrcRSSFeedView(),
+ name="feed"
+ ),
+ path(
+ r'topic/<str:slug>',
+ views.TopicListView.as_view(),
+ name="list_topics"
+ ),
+ #path(
+ # r'books/<str:slug>',
+ # views.BookDetailView.as_view(),
+ # name='detail_book'
+ #),
+ #path(
+ # r'books/',
+ # views.BookListView.as_view(),
+ # name='list_books'
+ #),
+ path(
+ r'<str:slug>.txt',
+ views.SrcDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ path(
+ r'<str:slug>',
+ views.SrcDetailView.as_view(),
+ name="detail"
+ ),
+ re_path(
+ r'<int:page>',
+ views.SrcListView.as_view(),
+ name="list"
+ ),
+ path(
+ r'',
+ views.SrcListView.as_view(),
+ {'page':1},
+ name="list"
+ ),
+]
diff --git a/app/posts/views/__init__.py b/app/posts/views/__init__.py
new file mode 100644
index 0000000..5fd984c
--- /dev/null
+++ b/app/posts/views/__init__.py
@@ -0,0 +1,19 @@
+from django.contrib.syndication.views import Feed
+
+from ..models import Post
+
+class PostRSSFeedView(Feed):
+ title = "Luxagraf: Topographical Writings"
+ link = "https://luxagraf.net/"
+ description = "Latest postings to luxagraf.net"
+ description_template = 'feeds/blog_description.html'
+
+ def items(self):
+ return Post.objects.filter(status__exact=1).order_by('-pub_date')[:10]
+
+ def item_pubdate(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ pubdate.
+ """
+ return item.pub_date
diff --git a/app/posts/views/field_note_views.py b/app/posts/views/field_note_views.py
new file mode 100644
index 0000000..41ceeaa
--- /dev/null
+++ b/app/posts/views/field_note_views.py
@@ -0,0 +1,39 @@
+from django.views.generic.dates import YearArchiveView, MonthArchiveView
+from django.views.generic.detail import DetailView
+
+from utils.views import PaginatedListView, LuxDetailView
+
+from ..models import Post, PostType
+
+
+class FieldNoteListView(PaginatedListView):
+ model = Post
+ template_name = "posts/fieldnote_list.html"
+ """
+ Return a list of Notes in reverse chronological order
+ """
+ queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1).order_by('-pub_date')
+
+
+class FieldNoteDetailView(LuxDetailView):
+ model = Post
+ slug_field = "slug"
+ template_name = "posts/fieldnote_detail.html"
+
+
+class FieldNoteDetailViewTXT(FieldNoteDetailView):
+ template_name = "posts/entry_detail.txt"
+
+
+class FieldNoteYearArchiveView(YearArchiveView):
+ queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1)
+ date_field = "pub_date"
+ template_name = "posts/fieldnote_archive_list_date.html"
+ make_object_list = True
+
+
+class FieldNoteMonthArchiveView(MonthArchiveView):
+ queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1)
+ date_field = "pub_date"
+ make_object_list = True
+ template_name = "posts/fieldnote_archive_list_date.html"
diff --git a/app/posts/views/guide_views.py b/app/posts/views/guide_views.py
new file mode 100644
index 0000000..e2f98f3
--- /dev/null
+++ b/app/posts/views/guide_views.py
@@ -0,0 +1,96 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.contrib.syndication.views import Feed
+from django.apps import apps
+from django.conf import settings
+
+from utils.views import PaginatedListView, LuxDetailView
+
+from ..models import Post, PostType
+from taxonomy.models import Category
+
+
+class GuideListView(PaginatedListView):
+ """
+ Return a list of Entries in reverse chronological order
+ """
+ model = Post
+ template_name = "posts/guide_base.html"
+
+ def get_queryset(self):
+ queryset = super(GuideListView, self).get_queryset()
+ return queryset.filter(status__exact=1).filter(post_type__in=[PostType.REVIEW,PostType.FIELD_TEST]).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image')
+
+
+class ReviewsListView(GuideListView):
+ template_name = "posts/post.html"
+
+ def get_queryset(self):
+ queryset = super(ReviewsListView, self).get_queryset()
+ return queryset.filter(post_type__in=[0,1]).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image')
+
+ def get_context_data(self, **kwargs):
+ context = super(ReviewsListView, self).get_context_data(**kwargs)
+ context['archive_type'] = 'Field Tests'
+ return context
+
+
+class EssayListView(PaginatedListView):
+ model = Post
+ template_name = "posts/essay_list.html"
+
+ def get_queryset(self):
+ queryset = super(EssayListView, self).get_queryset()
+ return queryset.filter(site__domain='luxagraf.net').filter(post_type__in=[PostType.ESSAY]).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image')
+
+ def get_context_data(self, **kwargs):
+ '''
+ override for custom breadcrumb path
+ '''
+ # Call the base implementation first to get a context
+ context = super(EssayListView, self).get_context_data(**kwargs)
+ context['breadcrumbs'] = ('Essays',)
+ return context
+
+
+class PostDetailView(LuxDetailView):
+ model = Post
+ slug_field = "slug"
+
+ def get_queryset(self):
+ queryset = super(PostDetailView, self).get_queryset()
+ return queryset.select_related('location').prefetch_related('field_notes')
+
+ def get_context_data(self, **kwargs):
+ context = super(PostDetailView, self).get_context_data(**kwargs)
+ related = []
+ for obj in self.object.related.all():
+ model = apps.get_model(obj.post_model.app_label, obj.post_model.model)
+ related.append(model.objects.get(slug=obj.slug))
+ context['related'] = related
+ return context
+
+ def get_template_names(self):
+ obj = self.get_object()
+ return ["posts/%s_detail.html" % obj.get_post_type_display(), 'posts/post_detail.html']
+
+
+class PostDetailViewTXT(PostDetailView):
+ template_name = "posts/entry_detail.txt"
+
+
+class PostsRSSFeedView(Feed):
+ title = "VanLifeReviews.com: "
+ link = "/"
+ description = "Latest reviews, stories and guides"
+ description_template = 'feeds/blog_description.html'
+
+ def items(self):
+ return Post.objects.filter(status__exact=1).order_by('-pub_date')[:10]
+
+ def item_pubdate(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ pubdate.
+ """
+ return item.pub_date
diff --git a/app/posts/views/jrnl_views.py b/app/posts/views/jrnl_views.py
new file mode 100644
index 0000000..0dc2dc8
--- /dev/null
+++ b/app/posts/views/jrnl_views.py
@@ -0,0 +1,173 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.views.generic.dates import DateDetailView
+from django.urls import reverse
+from django.views.generic.dates import YearArchiveView, MonthArchiveView
+from django.contrib.syndication.views import Feed
+from django.apps import apps
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+from django.db.models import Q
+
+from utils.views import PaginatedListView
+
+#from ..models import Entry, HomepageCurrator, Home
+from ..models import Post, PostType
+from locations.models import LuxCheckIn, Country, Region, Location
+from sightings.models import Sighting
+
+
+class JrnlListView(PaginatedListView):
+ """
+ Return a list of Entries in reverse chronological order
+ """
+ model = Post
+ template_name = "posts/jrnl_list.html"
+ queryset = Post.objects.filter(post_type=PostType.JRNL).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image')
+
+ def get_context_data(self, **kwargs):
+ context = super(JrnlListView, self).get_context_data(**kwargs)
+ context['breadcrumbs'] = ['jrnl',]
+ return context
+
+class JrnlCountryListView(PaginatedListView):
+ """
+ Return a list of Entries by Country in reverse chronological order
+ """
+ model = Post
+ template_name = "posts/jrnl_list.html"
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(JrnlCountryListView, self).get_context_data(**kwargs)
+ try:
+ context['region'] = Country.objects.get(slug__exact=self.kwargs['slug'])
+ except:
+ context['region'] = Region.objects.get(slug__exact=self.kwargs['slug'])
+ context['breadcrumbs'] = ['jrnl',]
+ return context
+
+ def get_queryset(self):
+ try:
+ region = Country.objects.get(slug__exact=self.kwargs['slug'])
+ qs = Post.objects.filter(
+ post_type=PostType.JRNL,
+ status__exact=1,
+ location__state__country=region
+ ).order_by('-pub_date')
+ except:
+ region = Region.objects.get(slug__exact=self.kwargs['slug'])
+ qs = Post.objects.filter(
+ post_type=PostType.JRNL,
+ status__exact=1,
+ location__state__country__lux_region=region.id
+ ).order_by('-pub_date')
+ return qs
+
+
+class JrnlYearArchiveView(YearArchiveView):
+ queryset = Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).select_related()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+ template_name = "posts/jrnl_date.html"
+
+
+class JrnlMonthArchiveView(MonthArchiveView):
+ queryset = Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).select_related()
+ date_field = "pub_date"
+ allow_future = True
+ template_name = "posts/jrnl_date.html"
+
+
+class JrnlDetailView(DateDetailView):
+ model = Post
+ date_field = 'pub_date'
+ slug_field = "slug"
+ template_name = "posts/jrnl_detail.html"
+
+ def get_queryset(self):
+ queryset = super(JrnlDetailView, self).get_queryset()
+ return queryset.filter(post_type=PostType.JRNL).select_related('location').prefetch_related('field_notes').prefetch_related('books')
+
+ def get_object(self, queryset=None):
+ obj = get_object_or_404(
+ self.model,
+ slug=self.kwargs['slug'],
+ pub_date__month=self.kwargs['month'],
+ pub_date__year=self.kwargs['year']
+ )
+ self.location = obj.location
+ return obj
+
+ def get_context_data(self, **kwargs):
+ context = super(JrnlDetailView, self).get_context_data(**kwargs)
+ context['wildlife'] = Sighting.objects.filter(
+ Q(location=self.location) |
+ Q(location__in=Location.objects.filter(parent=self.location))
+ ).select_related().order_by('ap_id', 'ap__apclass__kind').distinct("ap")
+ related = []
+ for obj in self.object.related.all():
+ model = apps.get_model(obj.model_name.app_label, obj.model_name.model)
+ related.append(model.objects.get(slug=obj.slug, pub_date=obj.pub_date))
+ context['related'] = related
+ context['breadcrumbs'] = ("jrnl",)
+ context['crumb_url'] = reverse('jrnl:list')
+ return context
+
+
+class JrnlDetailViewTXT(JrnlDetailView):
+ template_name = "posts/jrnl_detail.txt"
+
+
+class JrnlLatestView(JrnlDetailView):
+ template_name = "posts/jrnl_latest.html"
+
+ def get_object(self, queryset=None):
+ obj = self.model.objects.filter(status=1).latest()
+ self.location = obj.location
+ return obj
+
+
+class JrnlRSSFeedView(Feed):
+ title = "Luxagraf: Topographical Writings"
+ link = "/jrnl/"
+ description = "Latest postings to luxagraf.net"
+ description_template = 'feeds/blog_description.html'
+
+ def items(self):
+ return Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).order_by('-pub_date')[:10]
+
+ def item_pubdate(self, item):
+ """
+ Takes an item, as returned by items(), and returns the item's
+ pubdate.
+ """
+ return item.pub_date
+
+'''
+class HomepageList(ListView):
+ """
+ Return a main entry and list of Entries in reverse chronological order
+ """
+ model = Entry
+
+ def get_home(self):
+ return Home.objects.filter(pk=1).prefetch_related('featured_image').select_related('featured').select_related('featured__location').get()
+
+ def get_queryset(self):
+ queryset = super(HomepageList, self).get_queryset()
+ self.home = self.get_home()
+ return queryset.filter(status__exact=1).order_by('-pub_date').exclude().select_related('location').select_related('featured_image')[1:9]
+
+ def get_template_names(self):
+ return ['%s' % self.home.template_name]
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(HomepageList, self).get_context_data(**kwargs)
+ context['homepage'] = self.home
+ context['location'] = LuxCheckIn.objects.latest()
+ context['IMAGES_URL'] = settings.IMAGES_URL
+ return context
+'''
diff --git a/app/posts/views/src_views.py b/app/posts/views/src_views.py
new file mode 100644
index 0000000..990088f
--- /dev/null
+++ b/app/posts/views/src_views.py
@@ -0,0 +1,97 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.contrib.syndication.views import Feed
+from django.urls import reverse
+from django.conf import settings
+
+#from paypal.standard.forms import PayPalPaymentsForm
+from utils.views import PaginatedListView
+from ..models import Post, PostType
+from taxonomy.models import Category
+
+
+class SrcListView(PaginatedListView):
+ model = Post
+ template_name="posts/src_list.html"
+
+ def get_queryset(self):
+ queryset = super(SrcListView, self).get_queryset()
+ return queryset.filter(post_type=PostType.SRC).filter(status__exact=1).order_by('-pub_date')
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(SrcListView, self).get_context_data(**kwargs)
+ #context['topics'] = Topic.objects.all()
+ context['breadcrumbs'] = ['src',]
+ return context
+
+
+class SrcDetailView(DetailView):
+ model = Post
+ slug_field = "slug"
+ template_name="posts/src_detail.html"
+
+
+class SrcDetailViewTXT(SrcDetailView):
+ template_name = "posts/jrnl_detail.txt"
+
+
+class TopicListView(ListView):
+ template_name = 'posts/topic_list.html'
+
+ def get_queryset(self):
+ return Post.objects.filter(topics__slug=self.kwargs['slug'],post_type=PostType.SRC)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(TopicListView, self).get_context_data(**kwargs)
+ context['topic'] = Category.objects.get(slug__exact=self.kwargs['slug'])
+ return context
+
+
+class SrcRSSFeedView(Feed):
+ title = "luxagraf:src Code and Technology"
+ link = "/src/"
+ description = "Latest postings to luxagraf.net/src"
+ description_template = 'feeds/blog_description.html'
+
+ def items(self):
+ return SrcPost.objects.filter(status__exact=1).order_by('-pub_date')[:10]
+
+
+"""
+class BookListView(ListView):
+ template_name = "archives/src_books.html"
+
+ def queryset(self):
+ return Book.objects.filter(status__exact=1)
+
+
+class BookDetailView(DetailView):
+ model = Book
+
+ def get_template_names(self):
+ book = self.get_object()
+ return [book.template_name]
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(BookDetailView, self).get_context_data(**kwargs)
+ book = self.get_object()
+ if book.price_sale < book.price:
+ price = book.price_sale
+ else:
+ price = book.price
+ #paypal_dict = {
+ # "business": settings.PAYPAL_RECEIVER_EMAIL,
+ # "amount": price,
+ # "item_name": book.title,
+ # "invoice": "unique-invoice-id",
+ # "notify_url": "https://luxagraf.net/src/paypal/" + reverse('src:paypal-ipn'),
+ # "return_url": "https://luxagraf.net/src/thank-you",
+ # "cancel_return": "https://luxagraf.net/src/books/",
+ #}
+ #context['paypal_form'] = PayPalPaymentsForm(initial=paypal_dict)
+ return context
+
+"""
diff --git a/app/taxonomy/admin.py b/app/taxonomy/admin.py
new file mode 100644
index 0000000..783584e
--- /dev/null
+++ b/app/taxonomy/admin.py
@@ -0,0 +1,54 @@
+from django.contrib import admin
+from .models import Category, LuxTag
+
+
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = (
+ 'name',
+ 'color_rgb',
+ )
+ fieldsets = (
+ ('Item', {
+ 'fields': (
+ 'name',
+ 'color_rgb',
+ 'slug',
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }
+ ),
+ )
+
+ class Media:
+ js = ('image-loader.js', 'next-prev-links.js')
+
+
+@admin.register(LuxTag)
+class LuxTagAdmin(admin.ModelAdmin):
+ list_display = (
+ 'name',
+ 'color_rgb',
+ )
+ fieldsets = (
+ ('Item', {
+ 'fields': (
+ 'name',
+ 'color_rgb',
+ 'slug',
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }
+ ),
+ )
+
+ class Media:
+ js = ('image-loader.js', 'next-prev-links.js')
diff --git a/app/taxonomy/migrations/0001_initial.py b/app/taxonomy/migrations/0001_initial.py
new file mode 100644
index 0000000..bde5e44
--- /dev/null
+++ b/app/taxonomy/migrations/0001_initial.py
@@ -0,0 +1,57 @@
+# Generated by Django 3.1.3 on 2020-11-30 22:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Category',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=250)),
+ ('pluralized_name', models.CharField(max_length=60, null=True)),
+ ('slug', models.SlugField(blank=True)),
+ ('color_rgb', models.CharField(blank=True, max_length=20)),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_updated', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Category',
+ 'verbose_name_plural': 'Categories',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxTag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100, unique=True, verbose_name='name')),
+ ('slug', models.SlugField(max_length=100, unique=True, verbose_name='slug')),
+ ('color_rgb', models.CharField(blank=True, max_length=20)),
+ ],
+ options={
+ 'verbose_name': 'Tag',
+ 'verbose_name_plural': 'Tags',
+ },
+ ),
+ migrations.CreateModel(
+ name='TaggedItems',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('object_id', models.IntegerField(db_index=True, verbose_name='object ID')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_tagged_items', to='contenttypes.contenttype', verbose_name='content type')),
+ ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_items', to='taxonomy.luxtag')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/app/taxonomy/migrations/0002_auto_20211006_2025.py b/app/taxonomy/migrations/0002_auto_20211006_2025.py
new file mode 100644
index 0000000..6e8f57a
--- /dev/null
+++ b/app/taxonomy/migrations/0002_auto_20211006_2025.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.2.8 on 2021-10-06 20:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('taxonomy', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='category',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='luxtag',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ migrations.AlterField(
+ model_name='taggeditems',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/app/taxonomy/migrations/__init__.py b/app/taxonomy/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/taxonomy/migrations/__init__.py
diff --git a/app/taxonomy/models.py b/app/taxonomy/models.py
new file mode 100644
index 0000000..4db3294
--- /dev/null
+++ b/app/taxonomy/models.py
@@ -0,0 +1,44 @@
+from django.contrib.gis.db import models
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.utils.functional import cached_property
+
+from taggit.models import TagBase, GenericTaggedItemBase
+
+
+class LuxTag(TagBase):
+ ''' override the default taggit model to add some color '''
+ color_rgb = models.CharField(max_length=20, blank=True)
+
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+ @cached_property
+ def get_absolute_url(self):
+ return reverse("taxonomy:cat-detail", kwargs={"slug": self.slug})
+
+
+class TaggedItems(GenericTaggedItemBase):
+ ''' necessary with custom tag model, lets you still use TaggableManager'''
+ tag = models.ForeignKey(LuxTag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE)
+
+
+class Category(models.Model):
+ """ Generic model for Categories """
+ name = models.CharField(max_length=250)
+ pluralized_name = models.CharField(max_length=60, null=True)
+ slug = models.SlugField(blank=True)
+ color_rgb = models.CharField(max_length=20, blank=True)
+ date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False)
+ date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = _("Category")
+ verbose_name_plural = _("Categories")
+
+ def get_absolute_url(self):
+ return reverse("taxonomy:cat-detail", kwargs={"slug": self.slug})
diff --git a/app/taxonomy/urls.py b/app/taxonomy/urls.py
new file mode 100644
index 0000000..882bd52
--- /dev/null
+++ b/app/taxonomy/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "taxonomy"
+
+urlpatterns = [
+ path(
+ r'<slug>',
+ views.CategoryDetailView.as_view(),
+ name="cat-detail"
+ ),
+]
diff --git a/app/taxonomy/views.py b/app/taxonomy/views.py
new file mode 100644
index 0000000..2d749ab
--- /dev/null
+++ b/app/taxonomy/views.py
@@ -0,0 +1,14 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.contrib.syndication.views import Feed
+from django.urls import reverse
+from django.conf import settings
+
+#from paypal.standard.forms import PayPalPaymentsForm
+
+from .models import Category
+
+
+class CategoryDetailView(DetailView):
+ model = Category
+ slug_field = "slug"
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/utils/__init__.py
diff --git a/app/utils/next_prev.py b/app/utils/next_prev.py
new file mode 100644
index 0000000..766add1
--- /dev/null
+++ b/app/utils/next_prev.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# from https://github.com/gregplaysguitar/django-next-prev/blob/master/next_prev.py
+
+from functools import partial
+
+from django.db import models
+
+if not locals().get('reduce'):
+ from functools import reduce
+
+__version__ = '1.0.1'
+VERSION = tuple(map(int, __version__.split('.')))
+
+
+def get_model_attr(instance, attr):
+ """Example usage: get_model_attr(instance, 'category__slug')"""
+ for field in attr.split('__'):
+ instance = getattr(instance, field)
+ return instance
+
+
+def next_or_prev_in_order(instance, qs=None, prev=False, loop=False):
+ """Get the next (or previous with prev=True) item for instance, from the
+ given queryset (which is assumed to contain instance) respecting
+ queryset ordering. If loop is True, return the first/last item when the
+ end/start is reached. """
+
+ if not qs:
+ qs = instance.__class__.objects.all()
+
+ if prev:
+ qs = qs.reverse()
+ lookup = 'lt'
+ else:
+ lookup = 'gt'
+
+ q_list = []
+ prev_fields = []
+
+ if qs.query.extra_order_by:
+ ordering = qs.query.extra_order_by
+ elif qs.query.order_by:
+ ordering = qs.query.order_by
+ elif qs.query.get_meta().ordering:
+ ordering = qs.query.get_meta().ordering
+ else:
+ ordering = []
+
+ ordering = list(ordering)
+
+ # if the ordering doesn't contain pk, append it and reorder the queryset
+ # to ensure consistency
+ if 'pk' not in ordering and '-pk' not in ordering:
+ ordering.append('pk')
+ qs = qs.order_by(*ordering)
+
+ for field in ordering:
+ if field[0] == '-':
+ this_lookup = (lookup == 'gt' and 'lt' or 'gt')
+ field = field[1:]
+ else:
+ this_lookup = lookup
+ q_kwargs = dict([(f, get_model_attr(instance, f))
+ for f in prev_fields])
+ key = "%s__%s" % (field, this_lookup)
+ q_kwargs[key] = get_model_attr(instance, field)
+ q_list.append(models.Q(**q_kwargs))
+ prev_fields.append(field)
+ try:
+ return qs.filter(reduce(models.Q.__or__, q_list))[0]
+ except IndexError:
+ length = qs.count()
+ if loop and length > 1:
+ # queryset is reversed above if prev
+ return qs[0]
+ return None
+
+
+next_in_order = partial(next_or_prev_in_order, prev=False)
+prev_in_order = partial(next_or_prev_in_order, prev=True)
diff --git a/app/utils/static/autocomplete.js b/app/utils/static/autocomplete.js
new file mode 100644
index 0000000..514a064
--- /dev/null
+++ b/app/utils/static/autocomplete.js
@@ -0,0 +1,10 @@
+function autoCompleteItems() {
+ var item = document.getElementById('id_ap');
+ var singlePresetOpts = new Choices(item, {
+ searchPlaceholderValue: 'Search for Animal',
+ placeholder: true,
+ });
+}
+document.addEventListener("DOMContentLoaded", function(event) {
+ autoCompleteItems();
+});
diff --git a/app/utils/static/choices.mapfix.css b/app/utils/static/choices.mapfix.css
new file mode 100644
index 0000000..266dc68
--- /dev/null
+++ b/app/utils/static/choices.mapfix.css
@@ -0,0 +1,20 @@
+.field-ap .related-widget-wrapper {
+ overflow: visible;
+ position: relative;
+ z-index: 1000;
+}
+.field-ap {
+ overflow: visible;
+ position: relative;
+ z-index: 4000;
+ *zoom: 1;
+}
+.field-ap:before {
+ content: " ";
+ display: table;
+}
+.field-ap:after {
+ content: " ";
+ display: table;
+ clear: both;
+}
diff --git a/app/utils/static/choices.min.css b/app/utils/static/choices.min.css
new file mode 100644
index 0000000..2aca509
--- /dev/null
+++ b/app/utils/static/choices.min.css
@@ -0,0 +1,5 @@
+.choices{position:relative;margin-bottom:24px;font-size:16px}
+.choices:focus{outline:none}
+.choices:last-child{margin-bottom:0}
+.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{
+background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:300px;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.5}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #00bcd4}.choices[data-type*=select-one]:after{content:"";height:0;width:0;border-style:solid;border-color:#333 transparent transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open:after{border-color:transparent transparent #333 transparent;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]:after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0 -4px 0 8px;padding-left:16px;border-left:1px solid #008fa1;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);background-size:8px;width:8px;line-height:1;opacity:.75;border-radius:0}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:300px;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:30px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#00bcd4;border:1px solid #00a5bb;color:#fff;word-break:break-all}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#00a5bb;border:1px solid #008fa1}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown{display:none;z-index:100;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all}.choices__list--dropdown.is-active{display:block}.is-open .choices__list--dropdown{border-color:#b7b7b7}.is-flipped .choices__list--dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable{padding-right:100px}.choices__list--dropdown .choices__item--selectable:after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable:after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted:after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus{outline:none}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}.choices__input:focus{outline:0}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5}.choices[data-type*=select-multiple] .choices__input.is-hidden,.choices[data-type*=select-one] .choices__input.is-hidden,.choices__input.is-hidden{display:none}
diff --git a/app/utils/static/choices.min.js b/app/utils/static/choices.min.js
new file mode 100644
index 0000000..197f18e
--- /dev/null
+++ b/app/utils/static/choices.min.js
@@ -0,0 +1,5 @@
+/*! choices.js v3.0.3 | (c) 2018 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Choices=t():e.Choices=t()}(this,function(){return function(e){function t(n){if(i[n])return i[n].exports;var s=i[n]={exports:{},id:n,loaded:!1};return e[n].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var i={};return t.m=e,t.c=i,t.p="/assets/scripts/dist/",t(0)}([function(e,t,i){e.exports=i(1)},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function o(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var a=function(){function e(e,t){for(var i=0;i<t.length;i++){var n=t[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,i,n){return i&&e(t.prototype,i),n&&e(t,n),t}}(),c=i(2),l=n(c),h=i(3),u=n(h),d=i(4),f=n(d),p=i(31),v=i(32);i(33);var m=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"[data-choice]",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(r(this,e),(0,v.isType)("String",t)){var n=document.querySelectorAll(t);if(n.length>1)for(var s=1;s<n.length;s++){var o=n[s];new e(o,i)}}var a={silent:!1,items:[],choices:[],renderChoiceLimit:-1,maxItemCount:-1,addItems:!0,removeItems:!0,removeItemButton:!1,editItems:!1,duplicateItems:!0,delimiter:",",paste:!0,searchEnabled:!0,searchChoices:!0,searchFloor:1,searchResultLimit:4,searchFields:["label","value"],position:"auto",resetScrollPosition:!0,regexFilter:null,shouldSort:!0,shouldSortItems:!1,sortFilter:v.sortByAlpha,placeholder:!0,placeholderValue:null,searchPlaceholderValue:null,prependValue:null,appendValue:null,renderSelectedChoices:"auto",loadingText:"Loading...",noResultsText:"No results found",noChoicesText:"No choices to choose from",itemSelectText:"Press to select",addItemText:function(e){return'Press Enter to add <b>"'+e+'"</b>'},maxItemText:function(e){return"Only "+e+" values can be added."},uniqueItemText:"Only unique values can be added.",classNames:{containerOuter:"choices",containerInner:"choices__inner",input:"choices__input",inputCloned:"choices__input--cloned",list:"choices__list",listItems:"choices__list--multiple",listSingle:"choices__list--single",listDropdown:"choices__list--dropdown",item:"choices__item",itemSelectable:"choices__item--selectable",itemDisabled:"choices__item--disabled",itemChoice:"choices__item--choice",placeholder:"choices__placeholder",group:"choices__group",groupHeading:"choices__heading",button:"choices__button",activeState:"is-active",focusState:"is-focused",openState:"is-open",disabledState:"is-disabled",highlightedState:"is-highlighted",hiddenState:"is-hidden",flippedState:"is-flipped",loadingState:"is-loading",noResults:"has-no-results",noChoices:"has-no-choices"},fuseOptions:{include:"score"},callbackOnInit:null,callbackOnCreateTemplates:null};if(this.idNames={itemChoice:"item-choice"},this.config=(0,v.extend)(a,i),"auto"!==this.config.renderSelectedChoices&&"always"!==this.config.renderSelectedChoices&&(this.config.silent||console.warn("renderSelectedChoices: Possible values are 'auto' and 'always'. Falling back to 'auto'."),this.config.renderSelectedChoices="auto"),this.store=new f.default(this.render),this.initialised=!1,this.currentState={},this.prevState={},this.currentValue="",this.element=t,this.passedElement=(0,v.isType)("String",t)?document.querySelector(t):t,!this.passedElement)return void(this.config.silent||console.error("Passed element not found"));this.isTextElement="text"===this.passedElement.type,this.isSelectOneElement="select-one"===this.passedElement.type,this.isSelectMultipleElement="select-multiple"===this.passedElement.type,this.isSelectElement=this.isSelectOneElement||this.isSelectMultipleElement,this.isValidElementType=this.isTextElement||this.isSelectElement,this.isIe11=!(!navigator.userAgent.match(/Trident/)||!navigator.userAgent.match(/rv[ :]11/)),this.isScrollingOnIe=!1,this.config.shouldSortItems===!0&&this.isSelectOneElement&&(this.config.silent||console.warn("shouldSortElements: Type of passed element is 'select-one', falling back to false.")),this.highlightPosition=0,this.canSearch=this.config.searchEnabled,this.placeholder=!1,this.isSelectOneElement||(this.placeholder=!!this.config.placeholder&&(this.config.placeholderValue||this.passedElement.getAttribute("placeholder"))),this.presetChoices=this.config.choices,this.presetItems=this.config.items,this.passedElement.value&&(this.presetItems=this.presetItems.concat(this.passedElement.value.split(this.config.delimiter))),this.baseId=(0,v.generateId)(this.passedElement,"choices-"),this.render=this.render.bind(this),this._onFocus=this._onFocus.bind(this),this._onBlur=this._onBlur.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onKeyDown=this._onKeyDown.bind(this),this._onClick=this._onClick.bind(this),this._onTouchMove=this._onTouchMove.bind(this),this._onTouchEnd=this._onTouchEnd.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseOver=this._onMouseOver.bind(this),this._onPaste=this._onPaste.bind(this),this._onInput=this._onInput.bind(this),this.wasTap=!0;var c="classList"in document.documentElement;c||this.config.silent||console.error("Choices: Your browser doesn't support Choices");var l=(0,v.isElement)(this.passedElement)&&this.isValidElementType;if(l){if("active"===this.passedElement.getAttribute("data-choice"))return;this.init()}else this.config.silent||console.error("Incompatible input passed")}return a(e,[{key:"init",value:function(){if(this.initialised!==!0){var e=this.config.callbackOnInit;this.initialised=!0,this._createTemplates(),this._createInput(),this.store.subscribe(this.render),this.render(),this._addEventListeners(),e&&(0,v.isType)("Function",e)&&e.call(this)}}},{key:"destroy",value:function(){if(this.initialised!==!1){this._removeEventListeners(),this.passedElement.classList.remove(this.config.classNames.input,this.config.classNames.hiddenState),this.passedElement.removeAttribute("tabindex");var e=this.passedElement.getAttribute("data-choice-orig-style");Boolean(e)?(this.passedElement.removeAttribute("data-choice-orig-style"),this.passedElement.setAttribute("style",e)):this.passedElement.removeAttribute("style"),this.passedElement.removeAttribute("aria-hidden"),this.passedElement.removeAttribute("data-choice"),this.passedElement.value=this.passedElement.value,this.containerOuter.parentNode.insertBefore(this.passedElement,this.containerOuter),this.containerOuter.parentNode.removeChild(this.containerOuter),this.clearStore(),this.config.templates=null,this.initialised=!1}}},{key:"renderGroups",value:function(e,t,i){var n=this,s=i||document.createDocumentFragment(),o=this.config.sortFilter;return this.config.shouldSort&&e.sort(o),e.forEach(function(e){var i=t.filter(function(t){return n.isSelectOneElement?t.groupId===e.id:t.groupId===e.id&&!t.selected});if(i.length>=1){var o=n._getTemplate("choiceGroup",e);s.appendChild(o),n.renderChoices(i,s,!0)}}),s}},{key:"renderChoices",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],s=t||document.createDocumentFragment(),r=this.config,a=r.renderSelectedChoices,c=r.searchResultLimit,l=r.renderChoiceLimit,h=this.isSearching?v.sortByScore:this.config.sortFilter,u=function(e){var t="auto"!==a||(i.isSelectOneElement||!e.selected);if(t){var n=i._getTemplate("choice",e);s.appendChild(n)}},d=e;"auto"!==a||this.isSelectOneElement||(d=e.filter(function(e){return!e.selected}));var f=d.reduce(function(e,t){return t.placeholder?e.placeholderChoices.push(t):e.normalChoices.push(t),e},{placeholderChoices:[],normalChoices:[]}),p=f.placeholderChoices,m=f.normalChoices;(this.config.shouldSort||this.isSearching)&&m.sort(h);var g=d.length,y=[].concat(o(p),o(m));this.isSearching?g=c:l>0&&!n&&(g=l);for(var b=0;b<g;b++)y[b]&&u(y[b]);return s}},{key:"renderItems",value:function(e){var t=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=i||document.createDocumentFragment();if(this.config.shouldSortItems&&!this.isSelectOneElement&&e.sort(this.config.sortFilter),this.isTextElement){var s=this.store.getItemsReducedToValues(e),o=s.join(this.config.delimiter);this.passedElement.setAttribute("value",o),this.passedElement.value=o}else{var r=document.createDocumentFragment();e.forEach(function(e){var i=t._getTemplate("option",e);r.appendChild(i)}),this.passedElement.innerHTML="",this.passedElement.appendChild(r)}return e.forEach(function(e){var i=t._getTemplate("item",e);n.appendChild(i)}),n}},{key:"render",value:function(){if(!this.store.isLoading()&&(this.currentState=this.store.getState(),this.currentState!==this.prevState)){if((this.currentState.choices!==this.prevState.choices||this.currentState.groups!==this.prevState.groups||this.currentState.items!==this.prevState.items)&&this.isSelectElement){var e=this.store.getGroupsFilteredByActive(),t=this.store.getChoicesFilteredByActive(),i=document.createDocumentFragment();this.choiceList.innerHTML="",this.config.resetScrollPosition&&(this.choiceList.scrollTop=0),e.length>=1&&this.isSearching!==!0?i=this.renderGroups(e,t,i):t.length>=1&&(i=this.renderChoices(t,i));var n=this.store.getItemsFilteredByActive(),s=this._canAddItem(n,this.input.value);if(i.childNodes&&i.childNodes.length>0)s.response?(this.choiceList.appendChild(i),this._highlightChoice()):this.choiceList.appendChild(this._getTemplate("notice",s.notice));else{var o=void 0,r=void 0;this.isSearching?(r=(0,v.isType)("Function",this.config.noResultsText)?this.config.noResultsText():this.config.noResultsText,o=this._getTemplate("notice",r,"no-results")):(r=(0,v.isType)("Function",this.config.noChoicesText)?this.config.noChoicesText():this.config.noChoicesText,o=this._getTemplate("notice",r,"no-choices")),this.choiceList.appendChild(o)}}if(this.currentState.items!==this.prevState.items){var a=this.store.getItemsFilteredByActive();if(this.itemList.innerHTML="",a&&a){var c=this.renderItems(a);c.childNodes&&this.itemList.appendChild(c)}}this.prevState=this.currentState}}},{key:"highlightItem",value:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(!e)return this;var i=e.id,n=e.groupId,s=n>=0?this.store.getGroupById(n):null;return this.store.dispatch((0,p.highlightItem)(i,!0)),t&&(s&&s.value?(0,v.triggerEvent)(this.passedElement,"highlightItem",{id:i,value:e.value,label:e.label,groupValue:s.value}):(0,v.triggerEvent)(this.passedElement,"highlightItem",{id:i,value:e.value,label:e.label})),this}},{key:"unhighlightItem",value:function(e){if(!e)return this;var t=e.id,i=e.groupId,n=i>=0?this.store.getGroupById(i):null;return this.store.dispatch((0,p.highlightItem)(t,!1)),n&&n.value?(0,v.triggerEvent)(this.passedElement,"unhighlightItem",{id:t,value:e.value,label:e.label,groupValue:n.value}):(0,v.triggerEvent)(this.passedElement,"unhighlightItem",{id:t,value:e.value,label:e.label}),this}},{key:"highlightAll",value:function(){var e=this,t=this.store.getItems();return t.forEach(function(t){e.highlightItem(t)}),this}},{key:"unhighlightAll",value:function(){var e=this,t=this.store.getItems();return t.forEach(function(t){e.unhighlightItem(t)}),this}},{key:"removeItemsByValue",value:function(e){var t=this;if(!e||!(0,v.isType)("String",e))return this;var i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.value===e&&t._removeItem(i)}),this}},{key:"removeActiveItems",value:function(e){var t=this,i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.active&&e!==i.id&&t._removeItem(i)}),this}},{key:"removeHighlightedItems",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.highlighted&&i.active&&(e._removeItem(i),t&&e._triggerChange(i.value))}),this}},{key:"showDropdown",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=document.body,i=document.documentElement,n=Math.max(t.scrollHeight,t.offsetHeight,i.clientHeight,i.scrollHeight,i.offsetHeight);this.containerOuter.classList.add(this.config.classNames.openState),this.containerOuter.setAttribute("aria-expanded","true"),this.dropdown.classList.add(this.config.classNames.activeState),this.dropdown.setAttribute("aria-expanded","true");var s=this.dropdown.getBoundingClientRect(),o=Math.ceil(s.top+window.scrollY+this.dropdown.offsetHeight),r=!1;return"auto"===this.config.position?r=o>=n:"top"===this.config.position&&(r=!0),r&&this.containerOuter.classList.add(this.config.classNames.flippedState),e&&this.canSearch&&document.activeElement!==this.input&&this.input.focus(),(0,v.triggerEvent)(this.passedElement,"showDropdown",{}),this}},{key:"hideDropdown",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=this.containerOuter.classList.contains(this.config.classNames.flippedState);return this.containerOuter.classList.remove(this.config.classNames.openState),this.containerOuter.setAttribute("aria-expanded","false"),this.dropdown.classList.remove(this.config.classNames.activeState),this.dropdown.setAttribute("aria-expanded","false"),t&&this.containerOuter.classList.remove(this.config.classNames.flippedState),e&&this.canSearch&&document.activeElement===this.input&&this.input.blur(),(0,v.triggerEvent)(this.passedElement,"hideDropdown",{}),this}},{key:"toggleDropdown",value:function(){var e=this.dropdown.classList.contains(this.config.classNames.activeState);return e?this.hideDropdown():this.showDropdown(!0),this}},{key:"getValue",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],i=this.store.getItemsFilteredByActive(),n=[];return i.forEach(function(i){e.isTextElement?n.push(t?i.value:i):i.active&&n.push(t?i.value:i)}),this.isSelectOneElement?n[0]:n}},{key:"setValue",value:function(e){var t=this;if(this.initialised===!0){var i=[].concat(o(e)),n=function(e){var i=(0,v.getType)(e);if("Object"===i){if(!e.value)return;t.isTextElement?t._addItem(e.value,e.label,e.id,void 0,e.customProperties,e.placeholder):t._addChoice(e.value,e.label,!0,!1,-1,e.customProperties,e.placeholder)}else"String"===i&&(t.isTextElement?t._addItem(e):t._addChoice(e,e,!0,!1,-1,null))};i.length>1?i.forEach(function(e){n(e)}):n(i[0])}return this}},{key:"setValueByChoice",value:function(e){var t=this;if(!this.isTextElement){var i=this.store.getChoices(),n=(0,v.isType)("Array",e)?e:[e];n.forEach(function(e){var n=i.find(function(t){return t.value===e});n?n.selected?t.config.silent||console.warn("Attempting to select choice already selected"):t._addItem(n.value,n.label,n.id,n.groupId,n.customProperties,n.placeholder,n.keyCode):t.config.silent||console.warn("Attempting to select choice that does not exist")})}return this}},{key:"setChoices",value:function(e,t,i){var n=this,s=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(this.initialised===!0&&this.isSelectElement){if(!(0,v.isType)("Array",e)||!t)return this;s&&this._clearChoices(),this._setLoading(!0),e&&e.length&&(this.containerOuter.classList.remove(this.config.classNames.loadingState),e.forEach(function(e){e.choices?n._addGroup(e,e.id||null,t,i):n._addChoice(e[t],e[i],e.selected,e.disabled,void 0,e.customProperties,e.placeholder)})),this._setLoading(!1)}return this}},{key:"clearStore",value:function(){return this.store.dispatch((0,p.clearAll)()),this}},{key:"clearInput",value:function(){return this.input.value&&(this.input.value=""),this.isSelectOneElement||this._setInputWidth(),!this.isTextElement&&this.config.searchEnabled&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0))),this}},{key:"enable",value:function(){if(this.initialised){this.passedElement.disabled=!1;var e=this.containerOuter.classList.contains(this.config.classNames.disabledState);e&&(this._addEventListeners(),this.passedElement.removeAttribute("disabled"),this.input.removeAttribute("disabled"),this.containerOuter.classList.remove(this.config.classNames.disabledState),this.containerOuter.removeAttribute("aria-disabled"),this.isSelectOneElement&&this.containerOuter.setAttribute("tabindex","0"))}return this}},{key:"disable",value:function(){if(this.initialised){this.passedElement.disabled=!0;var e=!this.containerOuter.classList.contains(this.config.classNames.disabledState);e&&(this._removeEventListeners(),this.passedElement.setAttribute("disabled",""),this.input.setAttribute("disabled",""),this.containerOuter.classList.add(this.config.classNames.disabledState),this.containerOuter.setAttribute("aria-disabled","true"),this.isSelectOneElement&&this.containerOuter.setAttribute("tabindex","-1"))}return this}},{key:"ajax",value:function(e){var t=this;return this.initialised===!0&&this.isSelectElement&&(requestAnimationFrame(function(){t._handleLoadingState(!0)}),e(this._ajaxCallback())),this}},{key:"_triggerChange",value:function(e){e&&(0,v.triggerEvent)(this.passedElement,"change",{value:e})}},{key:"_handleButtonAction",value:function(e,t){if(e&&t&&this.config.removeItems&&this.config.removeItemButton){var i=t.parentNode.getAttribute("data-id"),n=e.find(function(e){return e.id===parseInt(i,10)});this._removeItem(n),this._triggerChange(n.value),this.isSelectOneElement&&this._selectPlaceholderChoice()}}},{key:"_selectPlaceholderChoice",value:function(){var e=this.store.getPlaceholderChoice();e&&(this._addItem(e.value,e.label,e.id,e.groupId,null,e.placeholder),this._triggerChange(e.value))}},{key:"_handleItemAction",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(e&&t&&this.config.removeItems&&!this.isSelectOneElement){var s=t.getAttribute("data-id");e.forEach(function(e){e.id!==parseInt(s,10)||e.highlighted?n||e.highlighted&&i.unhighlightItem(e):i.highlightItem(e)}),document.activeElement!==this.input&&this.input.focus()}}},{key:"_handleChoiceAction",value:function(e,t){if(e&&t){var i=t.getAttribute("data-id"),n=this.store.getChoiceById(i),s=e[0]&&e[0].keyCode?e[0].keyCode:null,o=this.dropdown.classList.contains(this.config.classNames.activeState);if(n.keyCode=s,(0,v.triggerEvent)(this.passedElement,"choice",{choice:n}),n&&!n.selected&&!n.disabled){var r=this._canAddItem(e,n.value);r.response&&(this._addItem(n.value,n.label,n.id,n.groupId,n.customProperties,n.placeholder,n.keyCode),this._triggerChange(n.value))}this.clearInput(),o&&this.isSelectOneElement&&(this.hideDropdown(),this.containerOuter.focus())}}},{key:"_handleBackspace",value:function(e){if(this.config.removeItems&&e){var t=e[e.length-1],i=e.some(function(e){return e.highlighted});this.config.editItems&&!i&&t?(this.input.value=t.value,this._setInputWidth(),this._removeItem(t),this._triggerChange(t.value)):(i||this.highlightItem(t,!1),this.removeHighlightedItems(!0))}}},{key:"_canAddItem",value:function(e,t){var i=!0,n=(0,v.isType)("Function",this.config.addItemText)?this.config.addItemText(t):this.config.addItemText;(this.isSelectMultipleElement||this.isTextElement)&&this.config.maxItemCount>0&&this.config.maxItemCount<=e.length&&(i=!1,n=(0,v.isType)("Function",this.config.maxItemText)?this.config.maxItemText(this.config.maxItemCount):this.config.maxItemText),this.isTextElement&&this.config.addItems&&i&&this.config.regexFilter&&(i=this._regexFilter(t));var s=!e.some(function(e){return(0,v.isType)("String",t)?e.value===t.trim():e.value===t});return s||this.config.duplicateItems||this.isSelectOneElement||!i||(i=!1,n=(0,v.isType)("Function",this.config.uniqueItemText)?this.config.uniqueItemText(t):this.config.uniqueItemText),{response:i,notice:n}}},{key:"_handleLoadingState",value:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],t=this.itemList.querySelector("."+this.config.classNames.placeholder);e?(this.containerOuter.classList.add(this.config.classNames.loadingState),this.containerOuter.setAttribute("aria-busy","true"),this.isSelectOneElement?t?t.innerHTML=this.config.loadingText:(t=this._getTemplate("placeholder",this.config.loadingText),this.itemList.appendChild(t)):this.input.placeholder=this.config.loadingText):(this.containerOuter.classList.remove(this.config.classNames.loadingState),this.isSelectOneElement?t.innerHTML=this.placeholder||"":this.input.placeholder=this.placeholder||"")}},{key:"_ajaxCallback",value:function(){var e=this;return function(t,i,n){if(t&&i){var s=(0,v.isType)("Object",t)?[t]:t;s&&(0,v.isType)("Array",s)&&s.length?(e._handleLoadingState(!1),e._setLoading(!0),s.forEach(function(t){if(t.choices){var s=t.id||null;e._addGroup(t,s,i,n)}else e._addChoice(t[i],t[n],t.selected,t.disabled,void 0,t.customProperties,t.placeholder)}),e._setLoading(!1),e.isSelectOneElement&&e._selectPlaceholderChoice()):e._handleLoadingState(!1),e.containerOuter.removeAttribute("aria-busy")}}}},{key:"_searchChoices",value:function(e){var t=(0,v.isType)("String",e)?e.trim():e,i=(0,v.isType)("String",this.currentValue)?this.currentValue.trim():this.currentValue;if(t.length>=1&&t!==i+" "){var n=this.store.getSearchableChoices(),s=t,o=(0,v.isType)("Array",this.config.searchFields)?this.config.searchFields:[this.config.searchFields],r=Object.assign(this.config.fuseOptions,{keys:o}),a=new l.default(n,r),c=a.search(s);return this.currentValue=t,this.highlightPosition=0,this.isSearching=!0,this.store.dispatch((0,p.filterChoices)(c)),c.length}return 0}},{key:"_handleSearch",value:function(e){if(e){var t=this.store.getChoices(),i=t.some(function(e){return!e.active});if(this.input===document.activeElement)if(e&&e.length>=this.config.searchFloor){var n=0;this.config.searchChoices&&(n=this._searchChoices(e)),(0,v.triggerEvent)(this.passedElement,"search",{value:e,resultCount:n})}else i&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0)))}}},{key:"_addEventListeners",value:function(){document.addEventListener("keyup",this._onKeyUp),document.addEventListener("keydown",this._onKeyDown),document.addEventListener("click",this._onClick),document.addEventListener("touchmove",this._onTouchMove),document.addEventListener("touchend",this._onTouchEnd),document.addEventListener("mousedown",this._onMouseDown),document.addEventListener("mouseover",this._onMouseOver),this.isSelectOneElement&&(this.containerOuter.addEventListener("focus",this._onFocus),this.containerOuter.addEventListener("blur",this._onBlur)),this.input.addEventListener("input",this._onInput),this.input.addEventListener("paste",this._onPaste),this.input.addEventListener("focus",this._onFocus),this.input.addEventListener("blur",this._onBlur)}},{key:"_removeEventListeners",value:function(){document.removeEventListener("keyup",this._onKeyUp),document.removeEventListener("keydown",this._onKeyDown),document.removeEventListener("click",this._onClick),document.removeEventListener("touchmove",this._onTouchMove),document.removeEventListener("touchend",this._onTouchEnd),document.removeEventListener("mousedown",this._onMouseDown),document.removeEventListener("mouseover",this._onMouseOver),this.isSelectOneElement&&(this.containerOuter.removeEventListener("focus",this._onFocus),this.containerOuter.removeEventListener("blur",this._onBlur)),this.input.removeEventListener("input",this._onInput),this.input.removeEventListener("paste",this._onPaste),this.input.removeEventListener("focus",this._onFocus),this.input.removeEventListener("blur",this._onBlur)}},{key:"_setInputWidth",value:function(){this.placeholder?this.input.value&&this.input.value.length>=this.placeholder.length/1.25&&(this.input.style.width=(0,v.getWidthOfInput)(this.input)):this.input.style.width=(0,v.getWidthOfInput)(this.input)}},{key:"_onKeyDown",value:function(e){var t,i=this;if(e.target===this.input||this.containerOuter.contains(e.target)){var n=e.target,o=this.store.getItemsFilteredByActive(),r=this.input===document.activeElement,a=this.dropdown.classList.contains(this.config.classNames.activeState),c=this.itemList&&this.itemList.children,l=String.fromCharCode(e.keyCode),h=46,u=8,d=13,f=65,p=27,m=38,g=40,y=33,b=34,E=e.ctrlKey||e.metaKey;this.isTextElement||!/[a-zA-Z0-9-_ ]/.test(l)||a||this.showDropdown(!0),this.canSearch=this.config.searchEnabled;var _=function(){E&&c&&(i.canSearch=!1,i.config.removeItems&&!i.input.value&&i.input===document.activeElement&&i.highlightAll())},S=function(){if(i.isTextElement&&n.value){var t=i.input.value,s=i._canAddItem(o,t);s.response&&(a&&i.hideDropdown(),i._addItem(t),i._triggerChange(t),i.clearInput())}if(n.hasAttribute("data-button")&&(i._handleButtonAction(o,n),e.preventDefault()),a){e.preventDefault();var r=i.dropdown.querySelector("."+i.config.classNames.highlightedState);r&&(o[0]&&(o[0].keyCode=d),i._handleChoiceAction(o,r))}else i.isSelectOneElement&&(a||(i.showDropdown(!0),e.preventDefault()))},I=function(){a&&(i.toggleDropdown(),i.containerOuter.focus())},w=function(){if(a||i.isSelectOneElement){a||i.showDropdown(!0),i.canSearch=!1;var t=e.keyCode===g||e.keyCode===b?1:-1,n=e.metaKey||e.keyCode===b||e.keyCode===y,s=void 0;if(n)s=t>0?Array.from(i.dropdown.querySelectorAll("[data-choice-selectable]")).pop():i.dropdown.querySelector("[data-choice-selectable]");else{var o=i.dropdown.querySelector("."+i.config.classNames.highlightedState);s=o?(0,v.getAdjacentEl)(o,"[data-choice-selectable]",t):i.dropdown.querySelector("[data-choice-selectable]")}s&&((0,v.isScrolledIntoView)(s,i.choiceList,t)||i._scrollToChoice(s,t),i._highlightChoice(s)),e.preventDefault()}},T=function(){!r||e.target.value||i.isSelectOneElement||(i._handleBackspace(o),e.preventDefault())},C=(t={},s(t,f,_),s(t,d,S),s(t,p,I),s(t,m,w),s(t,y,w),s(t,g,w),s(t,b,w),s(t,u,T),s(t,h,T),t);C[e.keyCode]&&C[e.keyCode]()}}},{key:"_onKeyUp",value:function(e){if(e.target===this.input){var t=this.input.value,i=this.store.getItemsFilteredByActive(),n=this._canAddItem(i,t);if(this.isTextElement){var s=this.dropdown.classList.contains(this.config.classNames.activeState);if(t){if(n.notice){var o=this._getTemplate("notice",n.notice);this.dropdown.innerHTML=o.outerHTML}n.response===!0?s||this.showDropdown():!n.notice&&s&&this.hideDropdown()}else s&&this.hideDropdown()}else{var r=46,a=8;e.keyCode!==r&&e.keyCode!==a||e.target.value?this.canSearch&&n.response&&this._handleSearch(this.input.value):!this.isTextElement&&this.isSearching&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0)))}this.canSearch=this.config.searchEnabled}}},{key:"_onInput",value:function(){this.isSelectOneElement||this._setInputWidth()}},{key:"_onTouchMove",value:function(){this.wasTap===!0&&(this.wasTap=!1)}},{key:"_onTouchEnd",value:function(e){var t=e.target||e.touches[0].target,i=this.dropdown.classList.contains(this.config.classNames.activeState);this.wasTap===!0&&this.containerOuter.contains(t)&&(t!==this.containerOuter&&t!==this.containerInner||this.isSelectOneElement||(this.isTextElement?document.activeElement!==this.input&&this.input.focus():i||this.showDropdown(!0)),e.stopPropagation()),this.wasTap=!0}},{key:"_onMouseDown",value:function(e){var t=e.target;if(t===this.choiceList&&this.isIe11&&(this.isScrollingOnIe=!0),this.containerOuter.contains(t)&&t!==this.input){var i=void 0,n=this.store.getItemsFilteredByActive(),s=e.shiftKey;(i=(0,v.findAncestorByAttrName)(t,"data-button"))?this._handleButtonAction(n,i):(i=(0,v.findAncestorByAttrName)(t,"data-item"))?this._handleItemAction(n,i,s):(i=(0,v.findAncestorByAttrName)(t,"data-choice"))&&this._handleChoiceAction(n,i),e.preventDefault()}}},{key:"_onClick",value:function(e){var t=e.target,i=this.dropdown.classList.contains(this.config.classNames.activeState),n=this.store.getItemsFilteredByActive();if(this.containerOuter.contains(t))t.hasAttribute("data-button")&&this._handleButtonAction(n,t),i?this.isSelectOneElement&&t!==this.input&&!this.dropdown.contains(t)&&this.hideDropdown(!0):this.isTextElement?document.activeElement!==this.input&&this.input.focus():this.canSearch?this.showDropdown(!0):(this.showDropdown(),this.containerOuter.focus());else{var s=n.some(function(e){return e.highlighted});s&&this.unhighlightAll(),this.containerOuter.classList.remove(this.config.classNames.focusState),i&&this.hideDropdown()}}},{key:"_onMouseOver",value:function(e){(e.target===this.dropdown||this.dropdown.contains(e.target))&&e.target.hasAttribute("data-choice")&&this._highlightChoice(e.target)}},{key:"_onPaste",value:function(e){e.target!==this.input||this.config.paste||e.preventDefault()}},{key:"_onFocus",value:function(e){var t=this,i=e.target;if(this.containerOuter.contains(i)){var n=this.dropdown.classList.contains(this.config.classNames.activeState),s={text:function(){i===t.input&&t.containerOuter.classList.add(t.config.classNames.focusState)},"select-one":function(){t.containerOuter.classList.add(t.config.classNames.focusState),i===t.input&&(n||t.showDropdown())},"select-multiple":function(){i===t.input&&(t.containerOuter.classList.add(t.config.classNames.focusState),n||t.showDropdown(!0))}};s[this.passedElement.type]()}}},{key:"_onBlur",value:function(e){var t=this,i=e.target;if(this.containerOuter.contains(i)&&!this.isScrollingOnIe){var n=this.store.getItemsFilteredByActive(),s=this.dropdown.classList.contains(this.config.classNames.activeState),o=n.some(function(e){return e.highlighted}),r={text:function(){i===t.input&&(t.containerOuter.classList.remove(t.config.classNames.focusState),o&&t.unhighlightAll(),s&&t.hideDropdown())},"select-one":function(){t.containerOuter.classList.remove(t.config.classNames.focusState),i===t.containerOuter&&s&&!t.canSearch&&t.hideDropdown(),i===t.input&&s&&t.hideDropdown()},"select-multiple":function(){i===t.input&&(t.containerOuter.classList.remove(t.config.classNames.focusState),s&&t.hideDropdown(),o&&t.unhighlightAll())}};r[this.passedElement.type]()}else this.isScrollingOnIe=!1,this.input.focus()}},{key:"_regexFilter",value:function(e){if(!e)return!1;var t=this.config.regexFilter,i=new RegExp(t.source,"i");return i.test(e)}},{key:"_scrollToChoice",value:function(e,t){var i=this;if(e){var n=this.choiceList.offsetHeight,s=e.offsetHeight,o=e.offsetTop+s,r=this.choiceList.scrollTop+n,a=t>0?this.choiceList.scrollTop+o-r:e.offsetTop,c=function e(){var n=4,s=i.choiceList.scrollTop,o=!1,r=void 0,c=void 0;t>0?(r=(a-s)/n,c=r>1?r:1,i.choiceList.scrollTop=s+c,s<a&&(o=!0)):(r=(s-a)/n,c=r>1?r:1,i.choiceList.scrollTop=s-c,s>a&&(o=!0)),o&&requestAnimationFrame(function(i){e(i,a,t)})};requestAnimationFrame(function(e){c(e,a,t)})}}},{key:"_highlightChoice",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,i=Array.from(this.dropdown.querySelectorAll("[data-choice-selectable]")),n=t;if(i&&i.length){var s=Array.from(this.dropdown.querySelectorAll("."+this.config.classNames.highlightedState));s.forEach(function(t){t.classList.remove(e.config.classNames.highlightedState),t.setAttribute("aria-selected","false")}),n?this.highlightPosition=i.indexOf(n):(n=i.length>this.highlightPosition?i[this.highlightPosition]:i[i.length-1],n||(n=i[0])),n.classList.add(this.config.classNames.highlightedState),n.setAttribute("aria-selected","true"),this.containerOuter.setAttribute("aria-activedescendant",n.id)}}},{key:"_addItem",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:-1,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:-1,s=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,o=arguments.length>5&&void 0!==arguments[5]&&arguments[5],r=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null,a=(0,v.isType)("String",e)?e.trim():e,c=r,l=this.store.getItems(),h=t||a,u=parseInt(i,10)||-1,d=n>=0?this.store.getGroupById(n):null,f=l?l.length+1:1;return this.config.prependValue&&(a=this.config.prependValue+a.toString()),this.config.appendValue&&(a+=this.config.appendValue.toString()),this.store.dispatch((0,p.addItem)(a,h,f,u,n,s,o,c)),
+this.isSelectOneElement&&this.removeActiveItems(f),d&&d.value?(0,v.triggerEvent)(this.passedElement,"addItem",{id:f,value:a,label:h,groupValue:d.value,keyCode:c}):(0,v.triggerEvent)(this.passedElement,"addItem",{id:f,value:a,label:h,keyCode:c}),this}},{key:"_removeItem",value:function(e){if(!e||!(0,v.isType)("Object",e))return this;var t=e.id,i=e.value,n=e.label,s=e.choiceId,o=e.groupId,r=o>=0?this.store.getGroupById(o):null;return this.store.dispatch((0,p.removeItem)(t,s)),r&&r.value?(0,v.triggerEvent)(this.passedElement,"removeItem",{id:t,value:i,label:n,groupValue:r.value}):(0,v.triggerEvent)(this.passedElement,"removeItem",{id:t,value:i,label:n}),this}},{key:"_addChoice",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=arguments.length>3&&void 0!==arguments[3]&&arguments[3],s=arguments.length>4&&void 0!==arguments[4]?arguments[4]:-1,o=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,r=arguments.length>6&&void 0!==arguments[6]&&arguments[6],a=arguments.length>7&&void 0!==arguments[7]?arguments[7]:null;if("undefined"!=typeof e&&null!==e){var c=this.store.getChoices(),l=t||e,h=c?c.length+1:1,u=this.baseId+"-"+this.idNames.itemChoice+"-"+h;this.store.dispatch((0,p.addChoice)(e,l,h,s,n,u,o,r,a)),i&&this._addItem(e,l,h,void 0,o,r,a)}}},{key:"_clearChoices",value:function(){this.store.dispatch((0,p.clearChoices)())}},{key:"_addGroup",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"value",s=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"label",o=(0,v.isType)("Object",e)?e.choices:Array.from(e.getElementsByTagName("OPTION")),r=t?t:Math.floor((new Date).valueOf()*Math.random()),a=!!e.disabled&&e.disabled;o?(this.store.dispatch((0,p.addGroup)(e.label,r,!0,a)),o.forEach(function(e){var t=e.disabled||e.parentNode&&e.parentNode.disabled;i._addChoice(e[n],(0,v.isType)("Object",e)?e[s]:e.innerHTML,e.selected,t,r,e.customProperties,e.placeholder)})):this.store.dispatch((0,p.addGroup)(e.label,e.id,!1,e.disabled))}},{key:"_getTemplate",value:function(e){if(!e)return null;for(var t=this.config.templates,i=arguments.length,n=Array(i>1?i-1:0),s=1;s<i;s++)n[s-1]=arguments[s];return t[e].apply(t,n)}},{key:"_createTemplates",value:function(){var e=this,t=this.config.classNames,i={containerOuter:function(i){return(0,v.strToEl)('\n <div\n class="'+t.containerOuter+'"\n '+(e.isSelectElement?e.config.searchEnabled?'role="combobox" aria-autocomplete="list"':'role="listbox"':"")+'\n data-type="'+e.passedElement.type+'"\n '+(e.isSelectOneElement?'tabindex="0"':"")+'\n aria-haspopup="true"\n aria-expanded="false"\n dir="'+i+'"\n >\n </div>\n ')},containerInner:function(){return(0,v.strToEl)('\n <div class="'+t.containerInner+'"></div>\n ')},itemList:function(){var i,n=(0,u.default)(t.list,(i={},s(i,t.listSingle,e.isSelectOneElement),s(i,t.listItems,!e.isSelectOneElement),i));return(0,v.strToEl)('\n <div class="'+n+'"></div>\n ')},placeholder:function(e){return(0,v.strToEl)('\n <div class="'+t.placeholder+'">\n '+e+"\n </div>\n ")},item:function(i){var n,o=(0,u.default)(t.item,(n={},s(n,t.highlightedState,i.highlighted),s(n,t.itemSelectable,!i.highlighted),s(n,t.placeholder,i.placeholder),n));if(e.config.removeItemButton){var r;return o=(0,u.default)(t.item,(r={},s(r,t.highlightedState,i.highlighted),s(r,t.itemSelectable,!i.disabled),s(r,t.placeholder,i.placeholder),r)),(0,v.strToEl)('\n <div\n class="'+o+'"\n data-item\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n data-deletable\n '+(i.active?'aria-selected="true"':"")+"\n "+(i.disabled?'aria-disabled="true"':"")+"\n >\n "+i.label+'<!--\n --><button\n type="button"\n class="'+t.button+'"\n data-button\n aria-label="Remove item: \''+i.value+"'\"\n >\n Remove item\n </button>\n </div>\n ")}return(0,v.strToEl)('\n <div\n class="'+o+'"\n data-item\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n '+(i.active?'aria-selected="true"':"")+"\n "+(i.disabled?'aria-disabled="true"':"")+"\n >\n "+i.label+"\n </div>\n ")},choiceList:function(){return(0,v.strToEl)('\n <div\n class="'+t.list+'"\n dir="ltr"\n role="listbox"\n '+(e.isSelectOneElement?"":'aria-multiselectable="true"')+"\n >\n </div>\n ")},choiceGroup:function(e){var i=(0,u.default)(t.group,s({},t.itemDisabled,e.disabled));return(0,v.strToEl)('\n <div\n class="'+i+'"\n data-group\n data-id="'+e.id+'"\n data-value="'+e.value+'"\n role="group"\n '+(e.disabled?'aria-disabled="true"':"")+'\n >\n <div class="'+t.groupHeading+'">'+e.value+"</div>\n </div>\n ")},choice:function(i){var n,o=(0,u.default)(t.item,t.itemChoice,(n={},s(n,t.itemDisabled,i.disabled),s(n,t.itemSelectable,!i.disabled),s(n,t.placeholder,i.placeholder),n));return(0,v.strToEl)('\n <div\n class="'+o+'"\n data-select-text="'+e.config.itemSelectText+'"\n data-choice\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n '+(i.disabled?'data-choice-disabled aria-disabled="true"':"data-choice-selectable")+'\n id="'+i.elementId+'"\n '+(i.groupId>0?'role="treeitem"':'role="option"')+"\n >\n "+i.label+"\n </div>\n ")},input:function(){var e=(0,u.default)(t.input,t.inputCloned);return(0,v.strToEl)('\n <input\n type="text"\n class="'+e+'"\n autocomplete="off"\n autocapitalize="off"\n spellcheck="false"\n role="textbox"\n aria-autocomplete="list"\n >\n ')},dropdown:function(){var e=(0,u.default)(t.list,t.listDropdown);return(0,v.strToEl)('\n <div\n class="'+e+'"\n aria-expanded="false"\n >\n </div>\n ')},notice:function(e){var i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=(0,u.default)(t.item,t.itemChoice,(i={},s(i,t.noResults,"no-results"===n),s(i,t.noChoices,"no-choices"===n),i));return(0,v.strToEl)('\n <div class="'+o+'">\n '+e+"\n </div>\n ")},option:function(e){return(0,v.strToEl)('\n <option value="'+e.value+'" selected>'+e.label+"</option>\n ")}},n=this.config.callbackOnCreateTemplates,o={};n&&(0,v.isType)("Function",n)&&(o=n.call(this,v.strToEl)),this.config.templates=(0,v.extend)(i,o)}},{key:"_setLoading",value:function(e){this.store.dispatch((0,p.setIsLoading)(e))}},{key:"_createInput",value:function(){var e=this,t=this.passedElement.getAttribute("dir")||"ltr",i=this._getTemplate("containerOuter",t),n=this._getTemplate("containerInner"),s=this._getTemplate("itemList"),o=this._getTemplate("choiceList"),r=this._getTemplate("input"),a=this._getTemplate("dropdown");this.containerOuter=i,this.containerInner=n,this.input=r,this.choiceList=o,this.itemList=s,this.dropdown=a,this.passedElement.classList.add(this.config.classNames.input,this.config.classNames.hiddenState),this.passedElement.tabIndex="-1";var c=this.passedElement.getAttribute("style");if(Boolean(c)&&this.passedElement.setAttribute("data-choice-orig-style",c),this.passedElement.setAttribute("style","display:none;"),this.passedElement.setAttribute("aria-hidden","true"),this.passedElement.setAttribute("data-choice","active"),(0,v.wrap)(this.passedElement,n),(0,v.wrap)(n,i),this.isSelectOneElement?r.placeholder=this.config.searchPlaceholderValue||"":this.placeholder&&(r.placeholder=this.placeholder,r.style.width=(0,v.getWidthOfInput)(r)),this.config.addItems||this.disable(),i.appendChild(n),i.appendChild(a),n.appendChild(s),this.isTextElement||a.appendChild(o),this.isSelectMultipleElement||this.isTextElement?n.appendChild(r):this.canSearch&&a.insertBefore(r,a.firstChild),this.isSelectElement){var l=Array.from(this.passedElement.getElementsByTagName("OPTGROUP"));if(this.highlightPosition=0,this.isSearching=!1,this._setLoading(!0),l&&l.length)l.forEach(function(t){e._addGroup(t,t.id||null)});else{var h=Array.from(this.passedElement.options),u=this.config.sortFilter,d=this.presetChoices;h.forEach(function(e){d.push({value:e.value,label:e.innerHTML,selected:e.selected,disabled:e.disabled||e.parentNode.disabled,placeholder:e.hasAttribute("placeholder")})}),this.config.shouldSort&&d.sort(u);var f=d.some(function(e){return e.selected});d.forEach(function(t,i){if(e.isSelectOneElement){var n=f||!f&&i>0;e._addChoice(t.value,t.label,!n||t.selected,!!n&&t.disabled,void 0,t.customProperties,t.placeholder)}else e._addChoice(t.value,t.label,t.selected,t.disabled,void 0,t.customProperties,t.placeholder)})}this._setLoading(!1)}else this.isTextElement&&this.presetItems.forEach(function(t){var i=(0,v.getType)(t);if("Object"===i){if(!t.value)return;e._addItem(t.value,t.label,t.id,void 0,t.customProperties,t.placeholder)}else"String"===i&&e._addItem(t)})}}]),e}();e.exports=m},function(e,t,i){!function(t){"use strict";function i(){console.log.apply(console,arguments)}function n(e,t){var i;this.list=e,this.options=t=t||{};for(i in a)a.hasOwnProperty(i)&&("boolean"==typeof a[i]?this.options[i]=i in t?t[i]:a[i]:this.options[i]=t[i]||a[i])}function s(e,t,i){var n,r,a,c,l,h;if(t){if(a=t.indexOf("."),a!==-1?(n=t.slice(0,a),r=t.slice(a+1)):n=t,c=e[n],null!==c&&void 0!==c)if(r||"string"!=typeof c&&"number"!=typeof c)if(o(c))for(l=0,h=c.length;l<h;l++)s(c[l],r,i);else r&&s(c,r,i);else i.push(c)}else i.push(e);return i}function o(e){return"[object Array]"===Object.prototype.toString.call(e)}function r(e,t){t=t||{},this.options=t,this.options.location=t.location||r.defaultOptions.location,this.options.distance="distance"in t?t.distance:r.defaultOptions.distance,this.options.threshold="threshold"in t?t.threshold:r.defaultOptions.threshold,this.options.maxPatternLength=t.maxPatternLength||r.defaultOptions.maxPatternLength,this.pattern=t.caseSensitive?e:e.toLowerCase(),this.patternLen=e.length,this.patternLen<=this.options.maxPatternLength&&(this.matchmask=1<<this.patternLen-1,this.patternAlphabet=this._calculatePatternAlphabet())}var a={id:null,caseSensitive:!1,include:[],shouldSort:!0,searchFn:r,sortFn:function(e,t){return e.score-t.score},getFn:s,keys:[],verbose:!1,tokenize:!1,matchAllTokens:!1,tokenSeparator:/ +/g,minMatchCharLength:1,findAllMatches:!1};n.VERSION="2.7.3",n.prototype.set=function(e){return this.list=e,e},n.prototype.search=function(e){this.options.verbose&&i("\nSearch term:",e,"\n"),this.pattern=e,this.results=[],this.resultMap={},this._keyMap=null,this._prepareSearchers(),this._startSearch(),this._computeScore(),this._sort();var t=this._format();return t},n.prototype._prepareSearchers=function(){var e=this.options,t=this.pattern,i=e.searchFn,n=t.split(e.tokenSeparator),s=0,o=n.length;if(this.options.tokenize)for(this.tokenSearchers=[];s<o;s++)this.tokenSearchers.push(new i(n[s],e));this.fullSeacher=new i(t,e)},n.prototype._startSearch=function(){var e,t,i,n,s=this.options,o=s.getFn,r=this.list,a=r.length,c=this.options.keys,l=c.length,h=null;if("string"==typeof r[0])for(i=0;i<a;i++)this._analyze("",r[i],i,i);else for(this._keyMap={},i=0;i<a;i++)for(h=r[i],n=0;n<l;n++){if(e=c[n],"string"!=typeof e){if(t=1-e.weight||1,this._keyMap[e.name]={weight:t},e.weight<=0||e.weight>1)throw new Error("Key weight has to be > 0 and <= 1");e=e.name}else this._keyMap[e]={weight:1};this._analyze(e,o(h,e,[]),h,i)}},n.prototype._analyze=function(e,t,n,s){var r,a,c,l,h,u,d,f,p,v,m,g,y,b,E,_=this.options,S=!1;if(void 0!==t&&null!==t){a=[];var I=0;if("string"==typeof t){if(r=t.split(_.tokenSeparator),_.verbose&&i("---------\nKey:",e),this.options.tokenize){for(b=0;b<this.tokenSearchers.length;b++){for(f=this.tokenSearchers[b],_.verbose&&i("Pattern:",f.pattern),p=[],g=!1,E=0;E<r.length;E++){v=r[E],m=f.search(v);var w={};m.isMatch?(w[v]=m.score,S=!0,g=!0,a.push(m.score)):(w[v]=1,this.options.matchAllTokens||a.push(1)),p.push(w)}g&&I++,_.verbose&&i("Token scores:",p)}for(l=a[0],u=a.length,b=1;b<u;b++)l+=a[b];l/=u,_.verbose&&i("Token score average:",l)}d=this.fullSeacher.search(t),_.verbose&&i("Full text score:",d.score),h=d.score,void 0!==l&&(h=(h+l)/2),_.verbose&&i("Score average:",h),y=!this.options.tokenize||!this.options.matchAllTokens||I>=this.tokenSearchers.length,_.verbose&&i("Check Matches",y),(S||d.isMatch)&&y&&(c=this.resultMap[s],c?c.output.push({key:e,score:h,matchedIndices:d.matchedIndices}):(this.resultMap[s]={item:n,output:[{key:e,score:h,matchedIndices:d.matchedIndices}]},this.results.push(this.resultMap[s])))}else if(o(t))for(b=0;b<t.length;b++)this._analyze(e,t[b],n,s)}},n.prototype._computeScore=function(){var e,t,n,s,o,r,a,c,l,h=this._keyMap,u=this.results;for(this.options.verbose&&i("\n\nComputing score:\n"),e=0;e<u.length;e++){for(n=0,s=u[e].output,o=s.length,c=1,t=0;t<o;t++)r=s[t].score,a=h?h[s[t].key].weight:1,l=r*a,1!==a?c=Math.min(c,l):(n+=l,s[t].nScore=l);1===c?u[e].score=n/o:u[e].score=c,this.options.verbose&&i(u[e])}},n.prototype._sort=function(){var e=this.options;e.shouldSort&&(e.verbose&&i("\n\nSorting...."),this.results.sort(e.sortFn))},n.prototype._format=function(){var e,t,n,s,o=this.options,r=o.getFn,a=[],c=this.results,l=o.include;for(o.verbose&&i("\n\nOutput:\n\n",c),n=o.id?function(e){c[e].item=r(c[e].item,o.id,[])[0]}:function(){},s=function(e){var t,i,n,s,o,r=c[e];if(l.length>0){if(t={item:r.item},l.indexOf("matches")!==-1)for(n=r.output,t.matches=[],i=0;i<n.length;i++)s=n[i],o={indices:s.matchedIndices},s.key&&(o.key=s.key),t.matches.push(o);l.indexOf("score")!==-1&&(t.score=c[e].score)}else t=r.item;return t},e=0,t=c.length;e<t;e++)n(e),a.push(s(e));return a},r.defaultOptions={location:0,distance:100,threshold:.6,maxPatternLength:32},r.prototype._calculatePatternAlphabet=function(){var e={},t=0;for(t=0;t<this.patternLen;t++)e[this.pattern.charAt(t)]=0;for(t=0;t<this.patternLen;t++)e[this.pattern.charAt(t)]|=1<<this.pattern.length-t-1;return e},r.prototype._bitapScore=function(e,t){var i=e/this.patternLen,n=Math.abs(this.options.location-t);return this.options.distance?i+n/this.options.distance:n?1:i},r.prototype.search=function(e){var t,i,n,s,o,r,a,c,l,h,u,d,f,p,v,m,g,y,b,E,_,S,I,w=this.options;if(e=w.caseSensitive?e:e.toLowerCase(),this.pattern===e)return{isMatch:!0,score:0,matchedIndices:[[0,e.length-1]]};if(this.patternLen>w.maxPatternLength){if(y=e.match(new RegExp(this.pattern.replace(w.tokenSeparator,"|"))),b=!!y)for(_=[],t=0,S=y.length;t<S;t++)I=y[t],_.push([e.indexOf(I),I.length-1]);return{isMatch:b,score:b?.5:1,matchedIndices:_}}for(s=w.findAllMatches,o=w.location,n=e.length,r=w.threshold,a=e.indexOf(this.pattern,o),E=[],t=0;t<n;t++)E[t]=0;for(a!=-1&&(r=Math.min(this._bitapScore(0,a),r),a=e.lastIndexOf(this.pattern,o+this.patternLen),a!=-1&&(r=Math.min(this._bitapScore(0,a),r))),a=-1,m=1,g=[],h=this.patternLen+n,t=0;t<this.patternLen;t++){for(c=0,l=h;c<l;)this._bitapScore(t,o+l)<=r?c=l:h=l,l=Math.floor((h-c)/2+c);for(h=l,u=Math.max(1,o-l+1),d=s?n:Math.min(o+l,n)+this.patternLen,f=Array(d+2),f[d+1]=(1<<t)-1,i=d;i>=u;i--)if(v=this.patternAlphabet[e.charAt(i-1)],v&&(E[i-1]=1),f[i]=(f[i+1]<<1|1)&v,0!==t&&(f[i]|=(p[i+1]|p[i])<<1|1|p[i+1]),f[i]&this.matchmask&&(m=this._bitapScore(t,i-1),m<=r)){if(r=m,a=i-1,g.push(a),a<=o)break;u=Math.max(1,2*o-a)}if(this._bitapScore(t+1,o)>r)break;p=f}return _=this._getMatchedIndices(E),{isMatch:a>=0,score:0===m?.001:m,matchedIndices:_}},r.prototype._getMatchedIndices=function(e){for(var t,i=[],n=-1,s=-1,o=0,r=e.length;o<r;o++)t=e[o],t&&n===-1?n=o:t||n===-1||(s=o-1,s-n+1>=this.options.minMatchCharLength&&i.push([n,s]),n=-1);return e[o-1]&&o-1-n+1>=this.options.minMatchCharLength&&i.push([n,o-1]),i},e.exports=n}(this)},function(e,t,i){var n,s;!function(){"use strict";function i(){for(var e=[],t=0;t<arguments.length;t++){var n=arguments[t];if(n){var s=typeof n;if("string"===s||"number"===s)e.push(n);else if(Array.isArray(n))e.push(i.apply(null,n));else if("object"===s)for(var r in n)o.call(n,r)&&n[r]&&e.push(r)}}return e.join(" ")}var o={}.hasOwnProperty;"undefined"!=typeof e&&e.exports?e.exports=i:(n=[],s=function(){return i}.apply(t,n),!(void 0!==s&&(e.exports=s)))}()},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var i=0;i<t.length;i++){var n=t[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,i,n){return i&&e(t.prototype,i),n&&e(t,n),t}}(),a=i(5),c=i(26),l=n(c),h=function(){function e(){o(this,e),this.store=(0,a.createStore)(l.default,window.devToolsExtension?window.devToolsExtension():void 0)}return r(e,[{key:"getState",value:function(){return this.store.getState()}},{key:"dispatch",value:function(e){this.store.dispatch(e)}},{key:"subscribe",value:function(e){this.store.subscribe(e)}},{key:"isLoading",value:function(){var e=this.store.getState();return e.general.loading}},{key:"getItems",value:function(){var e=this.store.getState();return e.items}},{key:"getItemsFilteredByActive",value:function(){var e=this.getItems(),t=e.filter(function(e){return e.active===!0},[]);return t}},{key:"getItemsReducedToValues",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.getItems(),t=e.reduce(function(e,t){return e.push(t.value),e},[]);return t}},{key:"getChoices",value:function(){var e=this.store.getState();return e.choices}},{key:"getChoicesFilteredByActive",value:function(){var e=this.getChoices(),t=e.filter(function(e){return e.active===!0});return t}},{key:"getChoicesFilteredBySelectable",value:function(){var e=this.getChoices(),t=e.filter(function(e){return e.disabled!==!0});return t}},{key:"getSearchableChoices",value:function(){var e=this.getChoicesFilteredBySelectable();return e.filter(function(e){return e.placeholder!==!0})}},{key:"getChoiceById",value:function(e){if(e){var t=this.getChoicesFilteredByActive(),i=t.find(function(t){return t.id===parseInt(e,10)});return i}return!1}},{key:"getGroups",value:function(){var e=this.store.getState();return e.groups}},{key:"getGroupsFilteredByActive",value:function(){var e=this.getGroups(),t=this.getChoices(),i=e.filter(function(e){var i=e.active===!0&&e.disabled===!1,n=t.some(function(e){return e.active===!0&&e.disabled===!1});return i&&n},[]);return i}},{key:"getGroupById",value:function(e){var t=this.getGroups(),i=t.find(function(t){return t.id===e});return i}},{key:"getPlaceholderChoice",value:function(){var e=this.getChoices(),t=[].concat(s(e)).reverse().find(function(e){return e.placeholder===!0});return t}}]),e}();t.default=h,e.exports=h},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0,t.compose=t.applyMiddleware=t.bindActionCreators=t.combineReducers=t.createStore=void 0;var s=i(6),o=n(s),r=i(21),a=n(r),c=i(23),l=n(c),h=i(24),u=n(h),d=i(25),f=n(d),p=i(22);n(p);t.createStore=o.default,t.combineReducers=a.default,t.bindActionCreators=l.default,t.applyMiddleware=u.default,t.compose=f.default},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t,i){function n(){g===m&&(g=m.slice())}function o(){return v}function a(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return n(),g.push(e),function(){if(t){t=!1,n();var i=g.indexOf(e);g.splice(i,1)}}}function h(e){if(!(0,r.default)(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if("undefined"==typeof e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(y)throw new Error("Reducers may not dispatch actions.");try{y=!0,v=p(v,e)}finally{y=!1}for(var t=m=g,i=0;i<t.length;i++){var n=t[i];n()}return e}function u(e){if("function"!=typeof e)throw new Error("Expected the nextReducer to be a function.");p=e,h({type:l.INIT})}function d(){var e,t=a;return e={subscribe:function(e){function i(){e.next&&e.next(o())}if("object"!=typeof e)throw new TypeError("Expected the observer to be an object.");i();var n=t(i);return{unsubscribe:n}}},e[c.default]=function(){return this},e}var f;if("function"==typeof t&&"undefined"==typeof i&&(i=t,t=void 0),"undefined"!=typeof i){if("function"!=typeof i)throw new Error("Expected the enhancer to be a function.");return i(s)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var p=e,v=t,m=[],g=m,y=!1;return h({type:l.INIT}),f={dispatch:h,subscribe:a,getState:o,replaceReducer:u},f[c.default]=d,f}t.__esModule=!0,t.ActionTypes=void 0,t.default=s;var o=i(7),r=n(o),a=i(17),c=n(a),l=t.ActionTypes={INIT:"@@redux/INIT"}},function(e,t,i){function n(e){if(!r(e)||s(e)!=a)return!1;var t=o(e);if(null===t)return!0;var i=u.call(t,"constructor")&&t.constructor;return"function"==typeof i&&i instanceof i&&h.call(i)==d}var s=i(8),o=i(14),r=i(16),a="[object Object]",c=Function.prototype,l=Object.prototype,h=c.toString,u=l.hasOwnProperty,d=h.call(Object);e.exports=n},function(e,t,i){function n(e){return null==e?void 0===e?c:a:l&&l in Object(e)?o(e):r(e)}var s=i(9),o=i(12),r=i(13),a="[object Null]",c="[object Undefined]",l=s?s.toStringTag:void 0;e.exports=n},function(e,t,i){var n=i(10),s=n.Symbol;e.exports=s},function(e,t,i){var n=i(11),s="object"==typeof self&&self&&self.Object===Object&&self,o=n||s||Function("return this")();e.exports=o},function(e,t){(function(t){var i="object"==typeof t&&t&&t.Object===Object&&t;e.exports=i}).call(t,function(){return this}())},function(e,t,i){function n(e){var t=r.call(e,c),i=e[c];try{e[c]=void 0;var n=!0}catch(e){}var s=a.call(e);return n&&(t?e[c]=i:delete e[c]),s}var s=i(9),o=Object.prototype,r=o.hasOwnProperty,a=o.toString,c=s?s.toStringTag:void 0;e.exports=n},function(e,t){function i(e){return s.call(e)}var n=Object.prototype,s=n.toString;e.exports=i},function(e,t,i){var n=i(15),s=n(Object.getPrototypeOf,Object);e.exports=s},function(e,t){function i(e,t){return function(i){return e(t(i))}}e.exports=i},function(e,t){function i(e){return null!=e&&"object"==typeof e}e.exports=i},function(e,t,i){e.exports=i(18)},function(e,t,i){(function(e,n){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o,r=i(20),a=s(r);o="undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof e?e:n;var c=(0,a.default)(o);t.default=c}).call(t,function(){return this}(),i(19)(e))},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children=[],e.webpackPolyfill=1),e}},function(e,t){"use strict";function i(e){var t,i=e.Symbol;return"function"==typeof i?i.observable?t=i.observable:(t=i("observable"),i.observable=t):t="@@observable",t}Object.defineProperty(t,"__esModule",{value:!0}),t.default=i},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t){var i=t&&t.type,n=i&&'"'+i.toString()+'"'||"an action";return"Given action "+n+', reducer "'+e+'" returned undefined. To ignore an action, you must explicitly return the previous state. If you want this reducer to hold no value, you can return null instead of undefined.'}function o(e){Object.keys(e).forEach(function(t){var i=e[t],n=i(void 0,{type:a.ActionTypes.INIT});if("undefined"==typeof n)throw new Error('Reducer "'+t+"\" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.");var s="@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".");if("undefined"==typeof i(void 0,{type:s}))throw new Error('Reducer "'+t+'" returned undefined when probed with a random type. '+("Don't try to handle "+a.ActionTypes.INIT+' or other actions in "redux/*" ')+"namespace. They are considered private. Instead, you must return the current state for any unknown actions, unless it is undefined, in which case you must return the initial state, regardless of the action type. The initial state may not be undefined, but can be null.")})}function r(e){for(var t=Object.keys(e),i={},n=0;n<t.length;n++){var r=t[n];"function"==typeof e[r]&&(i[r]=e[r])}var a=Object.keys(i),c=void 0;try{o(i)}catch(e){c=e}return function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(c)throw c;for(var n=!1,o={},r=0;r<a.length;r++){var l=a[r],h=i[l],u=e[l],d=h(u,t);if("undefined"==typeof d){var f=s(l,t);throw new Error(f)}o[l]=d,n=n||d!==u}return n?o:e}}t.__esModule=!0,t.default=r;var a=i(6),c=i(7),l=(n(c),i(22));n(l)},function(e,t){"use strict";function i(e){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(e);try{throw new Error(e)}catch(e){}}t.__esModule=!0,t.default=i},function(e,t){"use strict";function i(e,t){return function(){return t(e.apply(void 0,arguments))}}function n(e,t){if("function"==typeof e)return i(e,t);if("object"!=typeof e||null===e)throw new Error("bindActionCreators expected an object or a function, instead received "+(null===e?"null":typeof e)+'. Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');for(var n=Object.keys(e),s={},o=0;o<n.length;o++){var r=n[o],a=e[r];"function"==typeof a&&(s[r]=i(a,t))}return s}t.__esModule=!0,t.default=n},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(){for(var e=arguments.length,t=Array(e),i=0;i<e;i++)t[i]=arguments[i];return function(e){return function(i,n,s){var r=e(i,n,s),c=r.dispatch,l=[],h={getState:r.getState,dispatch:function(e){return c(e)}};return l=t.map(function(e){return e(h)}),c=a.default.apply(void 0,l)(r.dispatch),o({},r,{dispatch:c})}}}t.__esModule=!0;var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var i=arguments[t];for(var n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n])}return e};t.default=s;var r=i(25),a=n(r)},function(e,t){"use strict";function i(){for(var e=arguments.length,t=Array(e),i=0;i<e;i++)t[i]=arguments[i];return 0===t.length?function(e){return e}:1===t.length?t[0]:t.reduce(function(e,t){return function(){return e(t.apply(void 0,arguments))}})}t.__esModule=!0,t.default=i},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var s=i(5),o=i(27),r=n(o),a=i(28),c=n(a),l=i(29),h=n(l),u=i(30),d=n(u),f=(0,s.combineReducers)({items:r.default,groups:c.default,choices:h.default,general:d.default}),p=function(e,t){var i=e;return"CLEAR_ALL"===t.type&&(i=void 0),f(i,t)};t.default=p},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_ITEM":var n=[].concat(i(e),[{id:t.id,choiceId:t.choiceId,groupId:t.groupId,value:t.value,label:t.label,active:!0,highlighted:!1,customProperties:t.customProperties,placeholder:t.placeholder||!1,keyCode:null}]);return n.map(function(e){return e.highlighted&&(e.highlighted=!1),e});case"REMOVE_ITEM":return e.map(function(e){return e.id===t.id&&(e.active=!1),e});case"HIGHLIGHT_ITEM":return e.map(function(e){return e.id===t.id&&(e.highlighted=t.highlighted),e});default:return e}};t.default=n},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_GROUP":return[].concat(i(e),[{id:t.id,value:t.value,active:t.active,disabled:t.disabled}]);case"CLEAR_CHOICES":return e.groups=[];default:return e}};t.default=n},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_CHOICE":return[].concat(i(e),[{id:t.id,elementId:t.elementId,groupId:t.groupId,value:t.value,label:t.label||t.value,disabled:t.disabled||!1,selected:!1,active:!0,score:9999,customProperties:t.customProperties,placeholder:t.placeholder||!1,keyCode:null}]);case"ADD_ITEM":var n=e;return t.activateOptions&&(n=e.map(function(e){return e.active=t.active,e})),t.choiceId>-1&&(n=e.map(function(e){return e.id===parseInt(t.choiceId,10)&&(e.selected=!0),e})),n;case"REMOVE_ITEM":return t.choiceId>-1?e.map(function(e){return e.id===parseInt(t.choiceId,10)&&(e.selected=!1),e}):e;case"FILTER_CHOICES":var s=t.results,o=e.map(function(e){return e.active=s.some(function(t){return t.item.id===e.id&&(e.score=t.score,!0)}),e});return o;case"ACTIVATE_CHOICES":return e.map(function(e){return e.active=t.active,e});case"CLEAR_CHOICES":return e.choices=[];default:return e}};t.default=n},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{loading:!1},t=arguments[1];switch(t.type){case"LOADING":return{loading:t.isLoading};default:return e}};t.default=i},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.addItem=function(e,t,i,n,s,o,r,a){return{type:"ADD_ITEM",value:e,label:t,id:i,choiceId:n,groupId:s,customProperties:o,placeholder:r,keyCode:a}},t.removeItem=function(e,t){return{type:"REMOVE_ITEM",id:e,choiceId:t}},t.highlightItem=function(e,t){return{type:"HIGHLIGHT_ITEM",id:e,highlighted:t}},t.addChoice=function(e,t,i,n,s,o,r,a,c){return{type:"ADD_CHOICE",value:e,label:t,id:i,groupId:n,disabled:s,elementId:o,customProperties:r,placeholder:a,keyCode:c}},t.filterChoices=function(e){return{type:"FILTER_CHOICES",results:e}},t.activateChoices=function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];return{type:"ACTIVATE_CHOICES",active:e}},t.clearChoices=function(){return{type:"CLEAR_CHOICES"}},t.addGroup=function(e,t,i,n){return{type:"ADD_GROUP",value:e,id:t,active:i,disabled:n}},t.clearAll=function(){return{type:"CLEAR_ALL"}},t.setIsLoading=function(e){return{type:"LOADING",isLoading:e}}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n=(t.capitalise=function(e){return e.replace(/\w\S*/g,function(e){return e.charAt(0).toUpperCase()+e.substr(1).toLowerCase()})},t.generateChars=function(e){for(var t="",i=0;i<e;i++){var n=a(0,36);t+=n.toString(36)}return t}),s=(t.generateId=function(e,t){var i=e.id||e.name&&e.name+"-"+n(2)||n(4);return i=i.replace(/(:|\.|\[|\]|,)/g,""),i=t+i},t.getType=function(e){return Object.prototype.toString.call(e).slice(8,-1)}),o=t.isType=function(e,t){var i=s(t);
+return void 0!==t&&null!==t&&i===e},r=(t.isNode=function(e){return"object"===("undefined"==typeof Node?"undefined":i(Node))?e instanceof Node:e&&"object"===("undefined"==typeof e?"undefined":i(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},t.isElement=function(e){return"object"===("undefined"==typeof HTMLElement?"undefined":i(HTMLElement))?e instanceof HTMLElement:e&&"object"===("undefined"==typeof e?"undefined":i(e))&&null!==e&&1===e.nodeType&&"string"==typeof e.nodeName},t.extend=function e(){for(var t={},i=arguments.length,n=function(i){for(var n in i)Object.prototype.hasOwnProperty.call(i,n)&&(o("Object",i[n])?t[n]=e(!0,t[n],i[n]):t[n]=i[n])},s=0;s<i;s++){var r=arguments[s];o("Object",r)&&n(r)}return t},t.whichTransitionEvent=function(){var e,t=document.createElement("fakeelement"),i={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in i)if(void 0!==t.style[e])return i[e]},t.whichAnimationEvent=function(){var e,t=document.createElement("fakeelement"),i={animation:"animationend",OAnimation:"oAnimationEnd",MozAnimation:"animationend",WebkitAnimation:"webkitAnimationEnd"};for(e in i)if(void 0!==t.style[e])return i[e]}),a=(t.getParentsUntil=function(e,t,i){for(var n=[];e&&e!==document;e=e.parentNode){if(t){var s=t.charAt(0);if("."===s&&e.classList.contains(t.substr(1)))break;if("#"===s&&e.id===t.substr(1))break;if("["===s&&e.hasAttribute(t.substr(1,t.length-1)))break;if(e.tagName.toLowerCase()===t)break}if(i){var o=i.charAt(0);"."===o&&e.classList.contains(i.substr(1))&&n.push(e),"#"===o&&e.id===i.substr(1)&&n.push(e),"["===o&&e.hasAttribute(i.substr(1,i.length-1))&&n.push(e),e.tagName.toLowerCase()===i&&n.push(e)}else n.push(e)}return 0===n.length?null:n},t.wrap=function(e,t){return t=t||document.createElement("div"),e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t.appendChild(e)},t.getSiblings=function(e){for(var t=[],i=e.parentNode.firstChild;i;i=i.nextSibling)1===i.nodeType&&i!==e&&t.push(i);return t},t.findAncestor=function(e,t){for(;(e=e.parentElement)&&!e.classList.contains(t););return e},t.findAncestorByAttrName=function(e,t){for(var i=e;i;){if(i.hasAttribute(t))return i;i=i.parentElement}return null},t.debounce=function(e,t,i){var n;return function(){var s=this,o=arguments,r=function(){n=null,i||e.apply(s,o)},a=i&&!n;clearTimeout(n),n=setTimeout(r,t),a&&e.apply(s,o)}},t.getElemDistance=function(e){var t=0;if(e.offsetParent)do t+=e.offsetTop,e=e.offsetParent;while(e);return t>=0?t:0},t.getElementOffset=function(e,t){var i=t;return i>1&&(i=1),i>0&&(i=0),Math.max(e.offsetHeight*i)},t.getAdjacentEl=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(e&&t){var n=e.parentNode.parentNode,s=Array.from(n.querySelectorAll(t)),o=s.indexOf(e),r=i>0?1:-1;return s[o+r]}},t.getScrollPosition=function(e){return"bottom"===e?Math.max((window.scrollY||window.pageYOffset)+(window.innerHeight||document.documentElement.clientHeight)):window.scrollY||window.pageYOffset},t.isInView=function(e,t,i){return this.getScrollPosition(t)>this.getElemDistance(e)+this.getElementOffset(e,i)},t.isScrolledIntoView=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(e){var n=void 0;return n=i>0?t.scrollTop+t.offsetHeight>=e.offsetTop+e.offsetHeight:e.offsetTop>=t.scrollTop}},t.stripHTML=function(e){var t=document.createElement("DIV");return t.innerHTML=e,t.textContent||t.innerText||""},t.addAnimation=function(e,t){var i=r(),n=function n(){e.classList.remove(t),e.removeEventListener(i,n,!1)};e.classList.add(t),e.addEventListener(i,n,!1)},t.getRandomNumber=function(e,t){return Math.floor(Math.random()*(t-e)+e)}),c=t.strToEl=function(){var e=document.createElement("div");return function(t){var i=t.trim(),n=void 0;for(e.innerHTML=i,n=e.children[0];e.firstChild;)e.removeChild(e.firstChild);return n}}();t.getWidthOfInput=function(e){var t=e.value||e.placeholder,i=e.offsetWidth;if(t){var n=c("<span>"+t+"</span>");if(n.style.position="absolute",n.style.padding="0",n.style.top="-9999px",n.style.left="-9999px",n.style.width="auto",n.style.whiteSpace="pre",document.body.contains(e)&&window.getComputedStyle){var s=window.getComputedStyle(e);s&&(n.style.fontSize=s.fontSize,n.style.fontFamily=s.fontFamily,n.style.fontWeight=s.fontWeight,n.style.fontStyle=s.fontStyle,n.style.letterSpacing=s.letterSpacing,n.style.textTransform=s.textTransform,n.style.padding=s.padding)}document.body.appendChild(n),t&&n.offsetWidth!==e.offsetWidth&&(i=n.offsetWidth+4),document.body.removeChild(n)}return i+"px"},t.sortByAlpha=function(e,t){var i=(e.label||e.value).toLowerCase(),n=(t.label||t.value).toLowerCase();return i<n?-1:i>n?1:0},t.sortByScore=function(e,t){return e.score-t.score},t.triggerEvent=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=new CustomEvent(t,{detail:i,bubbles:!0,cancelable:!0});return e.dispatchEvent(n)}},function(e,t){"use strict";!function(){function e(e,t){t=t||{bubbles:!1,cancelable:!1,detail:void 0};var i=document.createEvent("CustomEvent");return i.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),i}Array.from||(Array.from=function(){var e=Object.prototype.toString,t=function(t){return"function"==typeof t||"[object Function]"===e.call(t)},i=function(e){var t=Number(e);return isNaN(t)?0:0!==t&&isFinite(t)?(t>0?1:-1)*Math.floor(Math.abs(t)):t},n=Math.pow(2,53)-1,s=function(e){var t=i(e);return Math.min(Math.max(t,0),n)};return function(e){var i=this,n=Object(e);if(null==e)throw new TypeError("Array.from requires an array-like object - not null or undefined");var o,r=arguments.length>1?arguments[1]:void 0;if("undefined"!=typeof r){if(!t(r))throw new TypeError("Array.from: when provided, the second argument must be a function");arguments.length>2&&(o=arguments[2])}for(var a,c=s(n.length),l=t(i)?Object(new i(c)):new Array(c),h=0;h<c;)a=n[h],r?l[h]="undefined"==typeof o?r(a,h):r.call(o,a,h):l[h]=a,h+=1;return l.length=c,l}}()),Array.prototype.find||(Array.prototype.find=function(e){if(null==this)throw new TypeError("Array.prototype.find called on null or undefined");if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var t,i=Object(this),n=i.length>>>0,s=arguments[1],o=0;o<n;o++)if(t=i[o],e.call(s,t,o,i))return t}),e.prototype=window.Event.prototype,window.CustomEvent=e}()}])});
+//# sourceMappingURL=choices.min.js.map
diff --git a/app/utils/static/image-loader.js b/app/utils/static/image-loader.js
new file mode 100644
index 0000000..2744251
--- /dev/null
+++ b/app/utils/static/image-loader.js
@@ -0,0 +1,47 @@
+function add_images(){
+ var el = document.getElementById("id_body_markdown");
+ if (el){
+ var iframe='<iframe id="images_frame" frameborder="0" style="border: #dddddd 1px solid;margin-left: 20px;width:330px; height:720px;" src="/luximages/insert/?textarea='+el.id+'"></iframe>';
+ el.insertAdjacentHTML('afterend', iframe);
+ }
+
+ var featured_image = document.getElementById("id_featured_image")
+
+ if (featured_image) {
+ featured_image.querySelectorAll('li').forEach(function(element) {
+ var cur = element.dataset.imageid;
+ var loop = Number(element.dataset.loopcounter);
+ if (cur != "") {
+ if (loop <= 100) {
+ console.log(loop);
+ var request = new XMLHttpRequest();
+ request.open('GET', '/photos/luximage/data/admin/tn/'+cur+'/', true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ var el = element.getElementsByTagName('label')[0];
+ url = "url('"+data['url']+"');";
+ //console.log(url);
+ el.style.backgroundImage = 'url('+data["url"]+')';
+
+ //console.log(el.style);
+ } else {
+ console.log("server error", request.statusText);
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ }
+ }
+ });
+ }
+
+}
+document.addEventListener("DOMContentLoaded", function(event) {
+ add_images();
+ md = document.forms["entry_form"].elements["body_markdown"];
+ md.style.maxHeight = "300rem";
+ md.style.maxWidth = "300rem";
+});
diff --git a/app/utils/static/next-prev-links.js b/app/utils/static/next-prev-links.js
new file mode 100644
index 0000000..dddc879
--- /dev/null
+++ b/app/utils/static/next-prev-links.js
@@ -0,0 +1,60 @@
+function build_next_prev() {
+
+ var url = window.location.href
+ var cur = Number(url.split('/')[6]);
+ var app = url.split('/')[4];
+ var model = url.split('/')[5];
+ if (cur) {
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ style.innerHTML = '.np-container {padding-left: 0;} .prev, .next {display: inline-block; margin-right: .5em;} .prev:after { content: "|"; margin-left:.5em;} .prev a:before {content: "\u00AB"; margin-right: 3px;} .next a:after{content: "\u00BB"; margin-left: 3px;}';
+ document.getElementsByTagName('head')[0].appendChild(style);
+
+ json_url = '/admin/data/'+app+'/'+model+'/'+cur+'/';
+ //console.log(json_url);
+
+ var container = document.createElement("ul");
+ var next_li = document.createElement("li");
+ var next_link = document.createElement("a");
+ var prev_li = document.createElement("li");
+ var prev_link = document.createElement("a");
+ prev_li.className = "prev";
+ next_li.className = "next";
+ container.className = "np-container";
+ next_link.textContent = "Next";
+ prev_link.textContent = "Prev";
+
+ var request = new XMLHttpRequest();
+ request.open('GET', json_url, true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ next_link.href = data['next'];
+ prev_link.href = data['prev'];
+ if (data['next'] != '') {
+ next_li.appendChild(next_link);
+ }
+ if (data['prev']) {
+ prev_li.appendChild(prev_link);
+ }
+ } else {
+ console.log("server error");
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ container.appendChild(prev_li);
+ container.appendChild(next_li);
+ //console.log(container);
+ Array.from(document.getElementsByClassName('object-tools')).forEach(function(item) {
+ item.parentNode.insertBefore(container, item.nextSibling);
+ })
+ } else {
+ return;
+ }
+};
+document.addEventListener("DOMContentLoaded", function(event) {
+ build_next_prev();
+});
diff --git a/app/utils/urls.py b/app/utils/urls.py
new file mode 100644
index 0000000..7c37c5d
--- /dev/null
+++ b/app/utils/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+
+urlpatterns = [
+ path(
+ r'<str:app>/<str:model>/<int:pk>/',
+ views.nav_json,
+ name="admin_links"
+ ),
+]
diff --git a/app/utils/util.py b/app/utils/util.py
new file mode 100644
index 0000000..dc04b0b
--- /dev/null
+++ b/app/utils/util.py
@@ -0,0 +1,147 @@
+import re
+from django.contrib.gis.geos import GEOSGeometry
+from django.apps import apps
+from django.template.loader import render_to_string
+from django.conf import settings
+from bs4 import BeautifulSoup
+import markdown
+
+
+def markdown_to_html(txt):
+ md = markdown.Markdown(
+ extensions=[
+ 'markdown.extensions.fenced_code',
+ 'markdown.extensions.codehilite',
+ 'markdown.extensions.attr_list',
+ 'footnotes',
+ 'extra'
+ ],
+ extension_configs = {
+ 'markdown.extensions.codehilite': {
+ 'css_class': 'highlight',
+ 'linenums': False
+ },
+ },
+ output_format='html5',
+ safe_mode=False
+ )
+ return md.convert(txt)
+
+
+def convertll(lat, lon):
+ pnt = GEOSGeometry('POINT({0} {1})'.format(lon, lat), srid=4326)
+ pnt.transform(3857)
+ return pnt.y, pnt.x
+
+
+def get_latlon():
+ loc = apps.get_model('locations', 'LuxCheckIn').objects.latest()
+ lat_converted, lon_converted = convertll(loc.lat, loc.lon)
+ return lat_converted, lon_converted
+
+
+def extract_main_image(markdown):
+ soup = BeautifulSoup(markdown, 'html.parser')
+ try:
+ image = soup.find_all('img')[0]['id']
+ img_pk = image.split('image-')[1]
+ return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk)
+ except IndexError:
+ return None
+
+
+def parse_products(s):
+ soup = BeautifulSoup(s.group(), "lxml")
+ for div in soup.find_all('div'):
+ try:
+ p = apps.get_model('products', 'Product').objects.get(pk=int(div['id'].split("product-")[1]))
+ return render_to_string("products/snippet.html", {'object': p})
+ except KeyError:
+ return str(s)
+
+
+def parse_image(s):
+ soup = BeautifulSoup(s.group(), "lxml")
+ for img in soup.find_all('img'):
+ try:
+ cl = img['class']
+ #if cl[0] == 'postpic' or cl[0] == 'postpicright':
+ replacer = "[[base_url]]"
+ if replacer in str(img):
+ s = str(img).replace('[[base_url]]', settings.IMAGES_URL)
+ #print(s)
+ return s
+ else:
+ try:
+ image_id = img['id'].split("image-")[1]
+ i = apps.get_model('photos', 'LuxImage').objects.get(pk=image_id)
+ caption = False
+ exif = False
+ cluster_class = None
+ is_cluster = False
+ extra = None
+ if cl[0] == 'cluster':
+ css_class = cl[0]
+ is_cluster = True
+ cluster_class = cl[1]
+ try:
+ if cl[2] == 'caption':
+ caption = True
+ elif cl[2] == 'exif':
+ exif = True
+ else:
+ extra = cl[2]
+
+ if len(cl) > 3:
+ if cl[3] == 'exif':
+ exif = True
+ except:
+ pass
+ elif cl[0] != 'cluster' and len(cl) > 1:
+ css_class = cl[0]
+ if cl[1] == 'caption':
+ caption = True
+ if cl[1] == 'exif':
+ exif = True
+ elif cl[0] != 'cluster' and len(cl) > 2:
+ css_class = cl[0]
+ if cl[1] == 'caption':
+ caption = True
+ if cl[2] == 'exif':
+ exif = True
+ print('caption'+str(caption))
+ else:
+ css_class = cl[0]
+ return render_to_string("lib/img_%s.html" % css_class, {'image': i, 'caption': caption, 'exif': exif, 'is_cluster': is_cluster, 'cluster_class': cluster_class, 'extra': extra})
+ except KeyError:
+ ''' regular inline image, not a luximage '''
+ return str(img)
+ except KeyError:
+ ''' regular inline image, not a luximage '''
+ return str(img)
+
+
+def render_images(s):
+ s = re.sub('<img(.*)/>', parse_image, s)
+ return s
+
+def render_products(s):
+ s = re.sub('<div(.*)</div>', parse_products, s)
+ return s
+
+def parse_video(s):
+ soup = BeautifulSoup(s, "lxml")
+ if soup.find('video'):
+ return True
+ return False
+
+def parse_reg_bio_page():
+ content = requests.get("https://www.theregister.co.uk/Author/Scott-Gilbertson/")
+ soup = BeautifulSoup(content, 'html.parser')
+ try:
+ image = soup.find_all('img')[0]['id']
+ img_pk = image.split('image-')[1]
+ return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk)
+ except IndexError:
+ return None
+
diff --git a/app/utils/views.py b/app/utils/views.py
new file mode 100644
index 0000000..6b69b25
--- /dev/null
+++ b/app/utils/views.py
@@ -0,0 +1,105 @@
+from itertools import chain
+import json
+from django.urls import reverse
+from django.http import Http404, HttpResponse
+from django.views.generic import ListView, DetailView
+from django.apps import apps
+from django.shortcuts import render
+from django.template import RequestContext
+
+from media.models import LuxImage, LuxVideo, LuxAudio
+
+
+BREADCRUMBS = {
+ 'SrcPost':'SRC',
+ 'Book':'Book Notes',
+ 'Entry':'Jrnl',
+ 'NewsletterMailing':'lttr',
+ 'LuxImage':'lttr'
+}
+
+class PaginatedListView(ListView):
+ """
+ handles my own pagination system and breadcrumbs
+ """
+ context_object_name = 'object_list'
+
+ def dispatch(self, request, *args, **kwargs):
+ path = request.path.split('/')[1:-1]
+ if path[-1] == self.kwargs['page']:
+ path = "/".join(t for t in path[:-1])
+ request.page_url = "/" + path + '/%d/'
+ else:
+ request.page_url = request.path + '%d/'
+ request.page = int(self.kwargs['page'])
+ request.base_path = path
+ return super(PaginatedListView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ '''
+ Adds breadcrumb path to every view
+ '''
+ # Call the base implementation first to get a context
+ context = super(PaginatedListView, self).get_context_data(**kwargs)
+ print('model=', self.model)
+ try:
+ context['breadcrumbs'] = (BREADCRUMBS[self.model.__name__],)
+ except KeyError:
+ context['breadcrumbs'] = (self.model._meta.verbose_name_plural,)
+ return context
+
+
+class LuxDetailView(DetailView):
+ """
+ handles breadcrumbs for detail pages
+ """
+ def get_context_data(self, **kwargs):
+ '''
+ Adds breadcrumb path to every view
+ '''
+ # Call the base implementation first to get a context
+ context = super(LuxDetailView, self).get_context_data(**kwargs)
+ print(self.object._meta.verbose_name_plural)
+ try:
+ context['breadcrumbs'] = (BREADCRUMBS[self.object._meta.model],)
+ except KeyError:
+ if self.object._meta.verbose_name_plural == 'posts':
+ context['breadcrumbs'] = (self.object.get_post_type_display()+"s",)
+ context['crumb_url'] = "/%ss/" % self.object.get_post_type_display()
+ else:
+ context['breadcrumbs'] = (self.object._meta.verbose_name_plural,)
+ try:
+ context['crumb_url']
+ except KeyError:
+ try:
+ context['crumb_url'] = reverse('%s:list' % self.object._meta.verbose_name_plural.slugify())
+ except:
+ # special case for pages:
+ context['breadcrumbs'] = (self.object.title,)
+ context['crumb_url'] = None
+ return context
+
+
+def insert_image(request):
+ """
+ The view that handles the admin insert image/video feature
+ """
+ images = LuxImage.objects.all()[:80]
+ videos = LuxVideo.objects.all()[:10]
+ audio = LuxAudio.objects.all()[:40]
+ object_list = sorted(
+ chain(images, videos, audio),
+ key=lambda instance: instance.pub_date,
+ reverse=True
+ )
+ return render(request, 'admin/insert_images.html', {'object_list': object_list, 'textarea_id': request.GET['textarea']})
+
+
+def nav_json(request, app, model, pk):
+ model = apps.get_model(app_label=app, model_name=model)
+ p = model.objects.get(pk=pk)
+ data = {}
+ data['prev'] = p.get_previous_admin_url
+ data['next'] = p.get_next_admin_url
+ data = json.dumps(data)
+ return HttpResponse(data)
diff --git a/app/utils/widgets.py b/app/utils/widgets.py
new file mode 100644
index 0000000..f4a7a4a
--- /dev/null
+++ b/app/utils/widgets.py
@@ -0,0 +1,144 @@
+import os
+from django import forms
+from django.contrib import admin
+from django.contrib.admin.widgets import AdminFileWidget
+from django.contrib.gis.admin import OSMGeoAdmin
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
+from django.template import Context
+from django.forms.widgets import SelectMultiple
+from django.conf import settings
+
+import markdown
+
+from bs4 import BeautifulSoup
+from django.utils.module_loading import import_string
+
+
+class CustomSelectMultiple(SelectMultiple):
+ def render_options(self, choices, selected_choices):
+ if not selected_choices:
+ # there is CreatView and we have no selected choices - render all selected
+ render_option = self.render_option
+ else:
+ # there is UpdateView and we have selected choices - render as default
+ render_option = super(CustomSelectMultiple, self).render_option
+
+ selected_choices = set(force_text(v) for v in selected_choices)
+ output = []
+ for option_value, option_label in chain(self.choices, choices):
+ if isinstance(option_label, (list, tuple)):
+ output.append(format_html('<optgroup label="{0}">', force_text(option_value)))
+ for option in option_label:
+ output.append(render_option(selected_choices, *option))
+ output.append('</optgroup>')
+ else:
+
+ output.append(render_option(selected_choices, option_value, option_label))
+ return '\n'.join(output)
+
+ def render_option(self, selected_choices, option_value, option_label):
+ option_value = force_text(option_value)
+ selected_html = mark_safe(' selected="selected"')
+
+ return format_html('<option value="{0}"{1}>{2}</option>',
+ option_value,
+ selected_html,
+ force_text(option_label))
+
+
+class TagListFilter(admin.SimpleListFilter):
+ # Human-readable title which will be displayed in the
+ # right admin sidebar just above the filter options.
+ title = _('tag')
+
+ # Parameter for the filter that will be used in the URL query.
+ parameter_name = 'tag'
+
+ def lookups(self, request, model_admin):
+ """
+ Returns a list of tuples. The first element in each
+ tuple is the coded value for the option that will
+ appear in the URL query. The second element is the
+ human-readable name for the option that will appear
+ in the right sidebar.
+ """
+ tl = []
+ self.model_to_use = model_admin.model
+ for t in self.model_to_use.tags.all().order_by('name'):
+ tl += (t.name, t.name),
+ return tl
+
+ def queryset(self, request, queryset):
+ """
+ Returns the filtered queryset based on the value
+ provided in the query string and retrievable via
+ `self.value()`.
+ """
+ qs = self.model_to_use.objects.all()
+ try:
+ request.GET['tag']
+ return qs.filter(tags__name=self.value())
+ except:
+ return qs
+
+
+def thumbnail(image_path):
+ absolute_url = os.path.join(settings.IMAGES_URL, image_path[7:])
+ print(absolute_url)
+ return '<img style="max-width: 400px" src="%s" alt="%s" />' % (absolute_url, image_path)
+
+
+class ImageRadioSelect(forms.RadioSelect):
+ template_name = 'horizontal_select.html'
+
+
+class AdminImageWidget(AdminFileWidget):
+ """
+ A FileField Widget that displays an image instead of a file path
+ if the current file is an image.
+ """
+ def render(self, name, value, attrs=None):
+ output = []
+ file_name = str(value)
+ help_text = ''
+ if file_name:
+ file_path = '%s' % (file_name)
+ if attrs['id'] == 'id_thumbnail':
+ help_text = '160 wide'
+ if attrs['id'] == 'id_image':
+ help_text = '205px high'
+ output.append('<span>%s</span><a target="_blank" href="%s">%s</a>' % (help_text, file_path, thumbnail(file_name)))
+
+ output.append(super(AdminFileWidget, self).render(name, value, attrs))
+ return mark_safe(''.join(output))
+
+
+class LGEntryForm(forms.ModelForm):
+ class Meta:
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 40, 'cols': 100}),
+ 'featured_image': ImageRadioSelect,
+ }
+
+
+class LGEntryFormSmall(forms.ModelForm):
+ class Meta:
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 12, 'cols': 100}),
+ }
+
+
+class OLAdminBase(OSMGeoAdmin):
+ default_lon = -9285175
+ default_lat = 4025046
+ default_zoom = 15
+ units = True
+ scrollable = False
+ map_width = 700
+ map_height = 425
+ map_template = 'gis/admin/osm.html'
+ openlayers_url = '/static/admin/js/OpenLayers.js'
+
+