diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..dd5a5fc --- /dev/null +++ b/__init__.py @@ -0,0 +1,237 @@ +import binascii +from django.conf import settings +from django.urls.base import reverse +import os + +SHUFFLE_DISABLE = 0 +SHUFFLE_KEEP = 1 +SHUFFLE_ENABLE = 2 + +REPEAT_DISABLE = 0 +REPEAT_ENABLE = 1 + +SEARCH_DISABLE = 0 +SEARCH_KEEP = 1 + +RESP_TYPE_RAWITEM = 'raw' +RESP_TYPE_WEBNAIL = 'webnail' +RESP_TYPE_THUMBNAIL = 'thumbnail' +RESP_TYPE_USERVIEW = 'userview' +RESP_TYPE_INFOVIEW = 'infoview' +RESP_TYPE_DOWNLOAD = 'download' +RESP_TYPE_SEARCH = 'search' + +DATA_TYPE_ITEM = 'item' +DATA_TYPE_SEARCH = 'search' + + +def __get_settings__(): + from .models import Setting + try: + s = Setting.objects.get(id=1) + except Setting.DoesNotExist: + s = Setting(id=1) + s.save() + return s + + +def suspend_public(): + return __get_settings__().suspend_puplic + + +def show_image(): + return __get_settings__().show_image + + +def show_video(): + return __get_settings__().show_video + + +def show_audio(): + return __get_settings__().show_audio + + +def show_other(): + return __get_settings__().show_other + + +def dict2args(d): + if len(d) == 0: + return '' + al = [] + for key in d: + if d[key] is None: + al.append('%s' % key) + else: + al.append('%s=%s' % (key, d[key])) + return '?' + '&'.join(al) + + +def pygal_responses_kwargs(responsetype, datatype, rel_path): + kwargs = {'responsetype': responsetype, 'datatype': datatype} + if rel_path: + kwargs['rel_path'] = rel_path + return kwargs + + +def url_args(request, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE, flat=False): + args_d = {} + if repeat == REPEAT_ENABLE: + args_d['repeat'] = None + if shuffle == SHUFFLE_ENABLE: + args_d['shuffle'] = binascii.hexlify(os.urandom(24)).decode('utf-8') + elif shuffle == SHUFFLE_KEEP: + shuffle_id = get_shuffle_id(request) + if shuffle_id is not None: + args_d['shuffle'] = shuffle_id + if search == SEARCH_KEEP: + search_query = get_search_query(request) + if search_query is not None: + args_d['q'] = search_query + elif type(search) == str: + args_d['q'] = search + if flat: + args_d['flat'] = None + return args_d + + +def get_full_path(rel_path): + return os.path.join(settings.ITEM_ROOT, rel_path.replace('/', os.path.sep)) + + +def get_rel_path(full_path): + if full_path.startswith(settings.ITEM_ROOT): + return full_path[len(settings.ITEM_ROOT) + 1:].replace(os.path.sep, '/') + + +def set_thumbnail_size(request, value): + request.session[settings.SESSION_KEY_THUMBNAIL_SIZE] = value + + +def get_thumbnail_size(request): + return request.session.get(settings.SESSION_KEY_THUMBNAIL_SIZE, max(settings.THUMBNAIL_SIZES)) + + +def get_thumbnail_max_size(request): + return max(settings.THUMBNAIL_SIZES) + + +def set_webnail_size(request, value): + request.session[settings.SESSION_KEY_WEBNAIL_SIZE] = value + + +def get_webnail_size(request): + return request.session.get(settings.SESSION_KEY_WEBNAIL_SIZE, max(settings.WEBNAIL_SIZES)) + + +def get_datatype(request): + try: + return request.META['PATH_INFO'].split('/')[3] + except IndexError: + return None + + +def get_responsetype(request): + try: + return request.META['PATH_INFO'].split('/')[2] + except IndexError: + return None + + +def url_download(request, rel_path, flat=False): + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_DOWNLOAD, get_datatype(request), rel_path)) + dict2args(url_args(request, flat=flat, search=SEARCH_KEEP)) + + +def url_helpview(request, page): + return reverse('pygal-helpview', kwargs={'page': page}) + + +def url_infoview(request, rel_path, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE): + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_INFOVIEW, get_datatype(request), rel_path)) + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search)) + + +def url_item(request, rel_path): + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_RAWITEM, DATA_TYPE_ITEM, rel_path)) + + +def url_profile(request): + nxt = request.GET.get('next', request.get_full_path()) + return reverse('pygal-profile') + '?next=%s' % nxt + + +def url_repeatview(request, rel_path): + return url_userview(request, rel_path, repeat=REPEAT_ENABLE, search=SEARCH_KEEP) + '#openModal' + + +def url_search(request): + return reverse('pygal-responses', kwargs={'responsetype': RESP_TYPE_USERVIEW, 'datatype': DATA_TYPE_SEARCH}) + + +def url_thumbnail(request, rel_path): + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_THUMBNAIL, DATA_TYPE_ITEM, rel_path)) + + +def url_userview(request, rel_path, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE): + if shuffle in [SHUFFLE_DISABLE, SHUFFLE_ENABLE]: + return request.META.get('PATH_INFO') + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search)) + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_USERVIEW, get_datatype(request), rel_path)) + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search)) + + +def url_webnail(request, rel_path): + return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_WEBNAIL, DATA_TYPE_ITEM, rel_path)) + + +def url_favouriteview(request): + return url_search(request) + dict2args(url_args(request, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_DISABLE, search='favourite_of:%s' % request.user.username)) + + +def url_favourite_set(request, rel_path): + nxt = request.GET.get('next', request.get_full_path()) + return reverse('pygal-setfavourite', args=(1, rel_path, )) + '?next=%s' % nxt + + +def url_favourite_unset(request, rel_path): + nxt = request.GET.get('next', request.get_full_path()) + return reverse('pygal-setfavourite', args=(0, rel_path, )) + '?next=%s' % nxt + + +def url_addtag(request, rel_path): + nxt = request.GET.get('next', request.get_full_path()) + return reverse('pygal-addtag', args=(rel_path, )) + '?next=%s' % nxt + + +def url_tagedit(request, tag_id): + nxt = request.GET.get('next', request.get_full_path()) + return reverse('pygal-edittag', args=(tag_id, )) + '?next=%s' % nxt + + +def is_helpview(request): + return request.META['PATH_INFO'].startswith(reverse('pygal-helpview', kwargs={'page': '_'})[:-1]) + + +def is_infoview(request): + return get_responsetype(request) == RESP_TYPE_INFOVIEW + + +def is_repeatview(request): + return 'repeat' in request.GET + + +def is_favouriteview(request): + return get_datatype(request) == DATA_TYPE_SEARCH and get_search_query(request) == 'favourite_of:%s' % request.user.username + + +def is_searchview(request): + return not is_favouriteview(request) and get_datatype(request) == DATA_TYPE_SEARCH + + +def is_flat(request): + return 'flat' in request.GET + + +def get_shuffle_id(request): + return request.GET.get('shuffle') + + +def get_search_query(request): + return request.GET.get('q') diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..6862234 --- /dev/null +++ b/admin.py @@ -0,0 +1,111 @@ +from django.contrib import admin +from .models import Item, Tag, Setting, TYPE_AUDIO, TYPE_FOLDER, TYPE_IMAGE, TYPE_VIDEO + + +class ItemAdmin(admin.ModelAdmin): + list_display = ('rel_path', 'type', ) + search_fields = ('rel_path', ) + readonly_fields = ( + 'rel_path', + 'type', + 'artist_c', + 'album_c', + 'year_c', + 'title_c', + 'track_c', + 'duration_c', + 'genre_c', + 'bitrate_c', + 'num_audio_c', + 'num_folders_c', + 'num_images_c', + 'num_other_c', + 'num_videos_c', + 'sil_c', + 'camera_vendor_c', + 'camera_model_c', + 'width_c', + 'height_c', + 'exposure_program_c', + 'exposure_time_c', + 'iso_c', + 'f_number_c', + 'focal_length_c', + 'flash_c', + 'lon_c', + 'lat_c', + 'orientation_c', + 'ratio_c' + ) + + list_filter = ( + ('type', admin.ChoicesFieldListFilter), + ('public_access', admin.BooleanFieldListFilter), + ('read_access', admin.RelatedFieldListFilter), + ('modify_access', admin.RelatedFieldListFilter), + ) + + def get_fields(self, request, obj=None): + if obj is not None: + rv = ['rel_path', 'type', 'datetime_c'] + if obj.type == TYPE_FOLDER: + rv += ['public_access', 'read_access', 'modify_access'] + else: + rv += ['favourite_of'] + rv += ['size_c', 'uid_c', 'settings_c', 'data_version_c'] + if obj.type == TYPE_AUDIO: + rv += [ + 'artist_c', + 'album_c', + 'year_c', + 'title_c', + 'track_c', + 'duration_c', + 'genre_c', + 'bitrate_c', + ] + if obj.type == TYPE_FOLDER: + rv += [ + 'num_folders_c', + 'num_audio_c', + 'num_images_c', + 'num_other_c', + 'num_videos_c', + 'sil_c', + ] + if obj.type == TYPE_IMAGE: + rv += [ + 'camera_vendor_c', + 'camera_model_c', + 'width_c', + 'height_c', + 'lon_c', + 'lat_c', + 'exposure_program_c', + 'exposure_time_c', + 'iso_c', + 'focal_length_c', + 'f_number_c', + 'flash_c', + 'orientation_c', + ] + if obj.type == TYPE_VIDEO: + rv += [ + 'duration_c', + 'width_c', + 'height_c', + 'ratio_c' + ] + return rv + else: + return admin.ModelAdmin.get_fields(self, request, obj=obj) + + +class TagAdmin(admin.ModelAdmin): + list_display = ('item', 'text', ) + search_fields = ('item__rel_path', 'text', ) + + +admin.site.register(Item, ItemAdmin) +admin.site.register(Tag, TagAdmin) +admin.site.register(Setting) diff --git a/apps.py b/apps.py new file mode 100644 index 0000000..dcd43be --- /dev/null +++ b/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class PygalConfig(AppConfig): + name = 'pygal' + + def ready(self): + import pygal.signals diff --git a/context.py b/context.py new file mode 100644 index 0000000..1d9f6d8 --- /dev/null +++ b/context.py @@ -0,0 +1,135 @@ +from django.utils.translation import gettext as _ +from .help import actionbar as actionbar_add_help +import os +from themes import gray_icon_url, color_icon_url +from users.context import menubar as menubar_user +from users.context import PROFILE_ENTRY_UID +import pygal +from pygal.models import get_item_by_rel_path + +MY_FAVOURITES_ENTRY_UID = 'my_favourites-main' +HELP_UID = 'help-main' +NAVIGATION_ENTRY_UID = 'navigation-main-%s' +HOME_ENTRY_UID = 'home-main' +SEARCH_ENTRY_UID = 'search-main' +BACK_ENTRY_UID = 'back-main' + + +def context_adaption(context, request, rel_path, wrapper_instance=None, title='', current_help_page=None): + context[context.MENUBAR].append_entry(HELP_UID, _('Help'), color_icon_url(request, 'help.png'), pygal.url_helpview(request, 'main'), True, False) + menubar_user(context[context.MENUBAR], request) + menubar(context[context.MENUBAR], request, rel_path) + navigationbar(context[context.NAVIGATIONBAR], request, rel_path) + if wrapper_instance is None: + # Pages without direct connection to an item (e.g. Helpview, ...) + context.set_additional_title(title) + context[context.ACTIONBAR].append_entry( + BACK_ENTRY_UID, # uid + _('Back'), # name + color_icon_url(request, 'back.png'), # icon + 'javascript:history.back()', # url + True, # left + False # active + ) + else: + wrapper_instance.context_adaption(context) + # HELP + if pygal.is_helpview(request): + actionbar_add_help(context, request, current_help_page=current_help_page) + + +def menubar(bar, request, rel_path): + if request.user.is_authenticated: + bar.append_entry(*my_favourites_entry_parameters(request, rel_path)) + try: + bar.replace_entry(PROFILE_ENTRY_UID, *profile_entry_parameters(request)) + except ValueError: + pass # Profile entry does not exist, so exchange is not needed (e.g. no user is logged in) + + +def navigationbar(bar, request, path): + if pygal.is_favouriteview(request): + if path: + bar.append_entry(*navigation_entry_parameters(request, path, None)) + anchor = get_item_by_rel_path(path).name + else: + anchor = None + elif pygal.is_searchview(request): + if path: + bar.append_entry(*navigation_entry_parameters(request, path, None)) + anchor = get_item_by_rel_path(path).name + else: + anchor = None + else: + anchor = None + while len(path) > 0 and path != os.path.sep: + bar.append_entry(*navigation_entry_parameters(request, path, anchor)) + anchor = get_item_by_rel_path(path).name + path = os.path.dirname(path) + bar.append_entry(*home_entry_parameters(request, anchor)) + + +def profile_entry_parameters(request): + return ( + PROFILE_ENTRY_UID, # uid + request.user.username, # name + color_icon_url(request, 'user.png'), # icon + pygal.url_profile(request), # url + False, # left + False # active + ) + + +def home_entry_parameters(request, anchor=None): + if pygal.is_favouriteview(request): + icon = 'favourite.png' + elif pygal.is_searchview(request): + icon = 'search.png' + else: + icon = 'home.png' + return ( + HOME_ENTRY_UID, + ':', + gray_icon_url(request, icon), + pygal.url_userview(request, rel_path='', search=pygal.SEARCH_KEEP) + ('#%s' % anchor if anchor is not None else ''), + True, + False + ) + + +def search_entry_parameters(request): + return ( + SEARCH_ENTRY_UID, + ':', + gray_icon_url(request, 'search.png'), + pygal.url_search(request), + True, + False + ) + + +def navigation_entry_parameters(request, path, anchor=None): + return ( + NAVIGATION_ENTRY_UID % os.path.basename(path), # uid + '/' + os.path.basename(path), # name + None, # icon + pygal.url_userview(request, path, search=pygal.SEARCH_KEEP) + ('#%s' % anchor if anchor is not None else ''), # url + False, # left + False # active + ) + + +def my_favourites_entry_parameters(request, rel_path): + if pygal.is_favouriteview(request) and rel_path != '': + url_addon = '#%s' % get_item_by_rel_path(rel_path).name + else: + url_addon = '' + return ( + MY_FAVOURITES_ENTRY_UID, # uid + _('My Favourites'), # name + color_icon_url(request, 'favourite.png'), # icon + pygal.url_favouriteview(request) + url_addon, # url + True, # left + pygal.is_favouriteview(request) # active + ) + diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..7cda58c --- /dev/null +++ b/forms.py @@ -0,0 +1,65 @@ +from django import forms +from .models import Tag, is_valid_area + + +class TagForm(forms.ModelForm): + COORD_KEYS = ['topleft_x', 'topleft_y', 'bottomright_x', 'bottomright_y'] + + class Meta: + model = Tag + fields = ['text', 'topleft_x', 'topleft_y', 'bottomright_x', 'bottomright_y'] + widgets = { + 'topleft_x': forms.HiddenInput(), + 'topleft_y': forms.HiddenInput(), + 'bottomright_x': forms.HiddenInput(), + 'bottomright_y': forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + self.factor_to_original = kwargs.pop('factor_to_original') + super(TagForm, self).__init__(*args, **kwargs) + + def __get_area_value__(self, key): + if len(self.data): + rv = self.data.get(key) + else: + rv = self.get_initial_for_field(self.fields[key], key) + try: + return int(rv) + except TypeError: + return None + except ValueError: + return None + + @property + def value_tl_x(self): + return self.__get_area_value__('topleft_x') + + @property + def value_tl_y(self): + return self.__get_area_value__('topleft_y') + + @property + def value_br_x(self): + return self.__get_area_value__('bottomright_x') + + @property + def value_br_y(self): + return self.__get_area_value__('bottomright_y') + + @property + def has_valid_coordinates(self): + return is_valid_area(self.value_br_x, self.value_br_y, self.value_tl_x, self.value_tl_y) + + def save(self, commit=True): + if self.has_valid_coordinates: + self.instance.topleft_x = self.value_tl_x + self.instance.topleft_y = self.value_tl_y + self.instance.bottomright_x = self.value_br_x + self.instance.bottomright_y = self.value_br_y + else: + self.instance.topleft_x = None + self.instance.topleft_y = None + self.instance.bottomright_x = None + self.instance.bottomright_y = None + return forms.ModelForm.save(self, commit=commit) diff --git a/help.py b/help.py new file mode 100644 index 0000000..c1909e7 --- /dev/null +++ b/help.py @@ -0,0 +1,150 @@ +from django.utils.translation import gettext as _ +import mycreole +import pygal +from themes import color_icon_url + +# TODO: Add field descriptions including the filed list choices, if field has a list of limited values (e.g. flash: "fired", ...) +# TODO: Describe logic operator order and brackets if possible +# TODO: Expend Examples for pictures without flash and high f_number to get potentially good quality images. + +HELP_UID = 'help' + +MAIN = mycreole.render_simple(_(""" += PyGal + +**PyGal** is a File **Gal**ery visulisation tool implementes in **Py**thon. + +It is designed to visualise images and videos. Nevertheless it is also possible to \ +visualise audio files and all other files. + + +== Help +* [[faq|Frequently asked questions]] +* [[access|Access Conrtrol]] +* [[search|Help on Search]] + +""")) + +FAQ = mycreole.render_simple(_(""" += Frequently asked questions +==Repeat-Mode +On Problems with the Repeat-Mode, please check your Browser \ +[[https://www.google.com/search?q=autoplay+enable+browser|properties]] \ +for autoplay restrictions! +""")) + +ACCESS = mycreole.render_simple(_(""" += Access control +Access Control is defined on folder level. The rights given for a folder \ +are the rights for all items in that folder. The folder itself has no access \ +restrictions. The content of a folder is defined by the accessable items \ +recursive below that folder. + +**Example:** If a user has only read access to the folder "bar" below the folder "foo", \ +but not to "foo", he will see the folder "bar" inside the folder "foo" and all items in \ +"foo/bar", but not the items in the folder "foo". +""")) + +SEARCH = mycreole.render_simple(_(""" += Search +The search looks up full words in //Itemnames (name)// and //Tags (tag)// without giving \ +special search commands in the search string. + + +== Search-Fields +The useful search fields depend on the item type. Therefore some fields are given twice or \ +more in the different type depending lists. + +=== General search fields +* rel_path (TEXT): +* name (TEXT): +* type (TEXT): +* favourite_of (KEYWORD): +* datetime (DATETIME): +* size (NUMERIC): +* tag (KEYWORD): + +=== Image +* exposure_program (TEXT): +* exposure_time (NUMERIC): +* flash (TEXT): +* f_number (NUMERIC): +* focal_length (NUMERIC): +* lon (NUMERIC): +* lat (NUMERIC): +* height (NUMERIC): +* iso (NUMERIC): +* camera_vendor (TEXT): +* camera_model (TEXT): +* orientation (NUMERIC): +* width (NUMERIC): + +=== Audio +* album (TEXT): +* artist (TEXT): +* bitrate (NUMERIC): +* duration (NUMERIC): +* genre (TEXT): +* title (TEXT): +* track (NUMERIC): +* year (NUMERIC): + +=== Video +* ratio (NUMERIC): + + +== Search syntax (Whoosh) +=== Logic operators +* AND +** **Example:** "foo AND bar" - Search will find all items with foo and bar. +* OR +** **Example:** "foo OR bar" - Search will find all items with foo, bar or with foo and bar. +* NOT +** **Example:** "foo NOT bar" - Search will find all items with foo and no bar. +=== Search in specific fields +A search pattern like //foo:bar// does look for //bar// in the field named //foo//. + +This search pattern can also be combined with other search text via logical operators. +=== Search for specific content +* **Wildcards:** +* **Range:** +** From To: +** Above: +** Below: +* **Named constants:** +** //now//: Current date +** //-[num]y//: Current date minus [num] years +** ... + +== Examples +* [[/pygal/userview/search?q=type:video AND datetime:2018|type:video AND datetime:2018]] gives results with all videos in year 2018. +* [[/pygal/userview/search?q=datetime:[-2y to now]|datetime:[-2y to now]]] gives results with all item of the last two years. +* [[/pygal/userview/search?q=rel_path:2018*|rel_path:2018*]] gives results with all item having 2018 in their path. +* [[/pygal/userview/search?q=tag:Test|tag:Test]] gives results with all item having Test in their tags. +* [[/pygal/userview/search?q=datetime:2016 AND favourite_of:*|datetime:2016 AND favourite_of:*]] gives results with all item having 2018 in their tags or path and are favourite of someone. +""")) + +help_pages = { + 'main': MAIN, + 'faq': FAQ, + 'access': ACCESS, + 'search': SEARCH, +} + + +def actionbar(context, request, current_help_page=None, **kwargs): + actionbar_entries = ( + ('1', 'Main'), + ('2', 'Faq'), + ('3', 'Access'), + ('4', 'Search'), + ) + for num, name in actionbar_entries: + context[context.ACTIONBAR].append_entry( + HELP_UID + '-%s' % name.lower(), # uid + _(name), # name + color_icon_url(request, num + '.png'), # icon + pygal.url_helpview(request, name.lower()), # url + True, # left + name.lower() == current_help_page, # active + ) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..70b8ca8 Binary files /dev/null and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..5e08deb --- /dev/null +++ b/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,257 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-22 14:42+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \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" + +#: context.py:48 views/__init__.py:60 views/pygal_help.py:6 +msgid "Help" +msgstr "Hilfe" + +#: templates/pygal/profile.html:8 +msgid "Xnail-Size" +msgstr "Bildgrößen" + +#: templates/pygal/profile.html:9 +msgid "Thumbnail Size" +msgstr "Bildgröße in der Übersicht" + +#: templates/pygal/profile.html:16 +msgid "Webnail Size" +msgstr "Bildgröße in der Ansicht" + +#: templates/pygal/profile.html:23 +msgid "Save" +msgstr "Speichern" + +#: views/__init__.py:23 +msgid "Access denied!" +msgstr "Zugriff verweigert!" + +#: views/__init__.py:24 views/__init__.py:84 views/__init__.py:121 +#: views/__init__.py:143 +msgid "" +"Access Denied: You either don't have access rights or the item does not " +"exist." +msgstr "" +"Zugriff verweigert: Entweder haben Sie keine Leseberechtigung oder es " +"exitieren keine Elemente." + +#: views/__init__.py:97 +#, python-format +msgid "The Thumbnail Size was set to \"%d\"" +msgstr "Die Bildgröße in der Übersicht wurde zu \"%d\" gesetzt" + +#: views/__init__.py:103 +#, python-format +msgid "The Webnail Size was set to \"%d\"" +msgstr "Die Bildgröße in der Ansicht wurde zu \"%d\" gesetzt" + +#: views/__init__.py:107 +#, python-format +msgid "Profile for %(username)s" +msgstr "Profil für %(username)s" + +#: views/infoviews.py:29 +msgid "General" +msgstr "Allgemein" + +#: views/infoviews.py:30 +msgid "Information" +msgstr "Informationen" + +#: views/infoviews.py:31 +msgid "Database" +msgstr "Datenbasis" + +#: views/infoviews.py:60 +msgid "Number of Folder(s)" +msgstr "Anzahl der Ordner" + +#: views/infoviews.py:61 +msgid "Number of Image(s)" +msgstr "Anzahl der Bilder" + +#: views/infoviews.py:62 +msgid "Number of Video(s)" +msgstr "Anzahl der Videos" + +#: views/infoviews.py:63 +msgid "Number of Audio File(s)" +msgstr "Anzahl der Audio-Dateien" + +#: views/infoviews.py:64 +msgid "Number of Other File(s)" +msgstr "Anzahl der anderen Dateien" + +#: views/infoviews.py:78 views/infoviews.py:86 views/infoviews.py:102 +#: views/userviews.py:70 +msgid "Creation Time" +msgstr "Erstellungsdatum" + +#: views/infoviews.py:79 views/infoviews.py:103 +msgid "Resolution" +msgstr "Auflösung" + +#: views/infoviews.py:80 +msgid "Camera" +msgstr "Kamera" + +#: views/infoviews.py:84 views/userviews.py:302 +msgid "GPS" +msgstr "GPS" + +#: views/infoviews.py:85 +msgid "Exposure Program" +msgstr "Aufnahme-Programm" + +#: views/infoviews.py:87 +msgid "Focal Length" +msgstr "Brennweite" + +#: views/infoviews.py:88 +msgid "Apeture" +msgstr "Blende" + +#: views/infoviews.py:89 +msgid "ISO" +msgstr "ISO" + +#: views/infoviews.py:90 +msgid "Flash" +msgstr "Blitz" + +#: views/infoviews.py:91 +msgid "Orientation" +msgstr "Ausrichtung" + +#: views/infoviews.py:104 views/infoviews.py:120 views/userviews.py:348 +msgid "Duration" +msgstr "Dauer" + +#: views/infoviews.py:105 +msgid "Ratio" +msgstr "Seitenverhältnis" + +#: views/infoviews.py:116 +msgid "Title" +msgstr "Titel" + +#: views/infoviews.py:117 views/userviews.py:345 +msgid "Artist" +msgstr "Interpret" + +#: views/infoviews.py:118 views/userviews.py:346 +msgid "Album" +msgstr "Album" + +#: views/infoviews.py:119 views/userviews.py:347 +msgid "Genre" +msgstr "Genre" + +#: views/infoviews.py:121 views/userviews.py:349 +msgid "Bitrate" +msgstr "Bitrate" + +#: views/infoviews.py:122 views/userviews.py:350 +msgid "Year" +msgstr "Jahr" + +#: views/infoviews.py:123 views/userviews.py:351 +msgid "Track" +msgstr "Titelnummer" + +#: views/pygal_help.py:8 +msgid "Repeat-Mode" +msgstr "Wiederholmodus" + +#: views/pygal_help.py:9 +msgid "" +"On Problems with the Repeat-Mode, please check your Browser properties " +"for autoplay restrictions!" +msgstr "" +"Bei Problemen mit dem Wiederholmodus, sollten Sie die Browsereinstellungen" +" prüfen." + +#: views/pygal_help.py:10 +msgid "Access Control" +msgstr "Zugangsberechtigungen" + +#: views/pygal_help.py:11 +msgid "" +"Access Control is defined on the folder level. The rights given for a folder " +"are the rights for all items in that folder. The folder itself has no access " +"restrictions. The content of a folder is defined by the accessable items " +"recursive below that folder." +msgstr "" +"Die Zugriffsrechte werden auf Ordnerebene definiert. Die Rechte des Ordners " +"sind die Rechte aller Inhalte dieses Ordners. Der Ordner selbst hat keine " +"Zugriffsberechtigungen. Der sichtbare Inhalt des Ordners wird durch die " +"darin befindlichen Elemente und die Inhalte der Unterordner definiert." + +#: views/pygal_help.py:12 +msgid "" +"Example: If a user has only read access to \"data/subfolder\" but not to " +"\"data\", he will see the folder \"subfolder\" in \"data\" and all items in " +"\"data/subfolder\"." +msgstr "" +"Beispiel: Besitzt ein Benutzer nur Zugriffsrechte auf \"data/subfolder\", " +"aber nicht zu \"data\", dann wird er den Ordners \"subfolder\" in \"data\" " +"und alle Elemente in \"subfolder\" sehen, aber nicht die Elemente im Ordner " +"\"data\"." + +#: views/userviews.py:69 +msgid "Name" +msgstr "Name" + +#: views/userviews.py:71 +msgid "Path" +msgstr "Pfad" + +#: views/userviews.py:72 +msgid "Size" +msgstr "Größe" + +#: views/userviews.py:73 +msgid "UID" +msgstr "UID" + +#: views/userviews.py:81 views/userviews.py:90 +msgid "Info" +msgstr "Info" + +#: views/userviews.py:98 +msgid "Download" +msgstr "Herunterladen" + +#: views/userviews.py:108 views/userviews.py:117 +msgid "Repeat" +msgstr "Wiederholen" + +#: views/userviews.py:126 views/userviews.py:135 +msgid "Shuffle" +msgstr "Durchmischen" + +#: views/userviews.py:255 +msgid "Download (Structured)" +msgstr "Herunterladen (Strukturiert)" + +#: views/userviews.py:264 +msgid "Download (Flat)" +msgstr "Herunterladen (Flach)" diff --git a/management/__init__.py b/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/commands/_private.py b/management/commands/_private.py new file mode 100644 index 0000000..b4119bf --- /dev/null +++ b/management/commands/_private.py @@ -0,0 +1,4 @@ +KEY_USERDATA_VERSION = 'version' +KEY_USERDATA_FAVOURITE = 'favourite' +KEY_USERDATA_TAGS = 'tag' +KEY_USERDATA_UPLOAD = 'upload' diff --git a/management/commands/convert_old_userdata.py b/management/commands/convert_old_userdata.py new file mode 100644 index 0000000..bd44c3d --- /dev/null +++ b/management/commands/convert_old_userdata.py @@ -0,0 +1,82 @@ +from django.core.management.base import BaseCommand +import fstools +import json +from pygal.models import is_valid_area, Item + +from ._private import KEY_USERDATA_VERSION +from ._private import KEY_USERDATA_FAVOURITE +from ._private import KEY_USERDATA_TAGS +from ._private import KEY_USERDATA_UPLOAD + +KEY_REL_PATH = '_rel_path_' +KEY_FAVOURITE = '_favourite_of_' +KEY_TAG = '_tags_' +KEY_UPLOAD = '_upload_' + + +class Command(BaseCommand): + help = 'Convert userdata from old pygal to JSON-Format.' + + def add_arguments(self, parser): + parser.add_argument('folder', nargs='+', type=str) + + def handle(self, *args, **options): + data = {} + data[KEY_USERDATA_VERSION] = 1 + path = options['folder'][0] + for f in fstools.filelist(path, '*.json'): + with open(f, 'r') as fh: + o = json.load(fh) + _rel_path_ = o.get(KEY_REL_PATH) + _favourite_of_ = o.get(KEY_FAVOURITE) + _tags_ = o.get(KEY_TAG) + _upload_ = o.get(KEY_UPLOAD) + if _rel_path_: + try: + i = Item.objects.get(rel_path=_rel_path_) + except Item.DoesNotExist: + self.stderr.write(self.style.ERROR('%s does not exist. No data taken over!' % _rel_path_)) + else: + if _favourite_of_: + for username in _favourite_of_: + if KEY_USERDATA_FAVOURITE not in data: + data[KEY_USERDATA_FAVOURITE] = {} + if username not in data[KEY_USERDATA_FAVOURITE]: + data[KEY_USERDATA_FAVOURITE][username] = [] + data[KEY_USERDATA_FAVOURITE][username].append(_rel_path_) + if len(_tags_): + new_tags = [] + for t in _tags_: + new_tag = {} + new_tag['text'] = _tags_[t].get('tag') + # + x = _tags_[t].get('x') + y = _tags_[t].get('y') + w = _tags_[t].get('w') + h = _tags_[t].get('h') + if x is not None and y is not None and w is not None and h is not None: + width = i.item_data.width + height = i.item_data.height + x1 = int(x * width) + y1 = int(y * height) + x2 = x1 + int(w * width) + y2 = y1 + int(h * height) + if is_valid_area(x1, y1, x2, y2): + new_tag['topleft_x'] = x1 + new_tag['topleft_y'] = y1 + new_tag['bottomright_x'] = x2 + new_tag['bottomright_y'] = y2 + new_tags.append(new_tag) + else: + self.stderr.write(self.style.ERROR('Coordinates are not valid for %s. Not transferred!' % _rel_path_)) + else: + new_tags.append(new_tag) + if len(new_tags) > 0: + if KEY_USERDATA_TAGS not in data: + data[KEY_USERDATA_TAGS] = {} + data[KEY_USERDATA_TAGS][_rel_path_] = new_tags + if len(_upload_): + if KEY_USERDATA_UPLOAD not in data: + data[KEY_USERDATA_UPLOAD] = {} + data[KEY_USERDATA_UPLOAD][_rel_path_] = _upload_ + self.stdout.write(json.dumps(data, indent=4, sort_keys=True)) diff --git a/management/commands/delete_lost_items.py b/management/commands/delete_lost_items.py new file mode 100644 index 0000000..892c9fd --- /dev/null +++ b/management/commands/delete_lost_items.py @@ -0,0 +1,28 @@ +from django.core.management.base import BaseCommand +import os +import pygal +from pygal.models import Item + + +class Command(BaseCommand): + def handle(self, *args, **options): + lil = self.lost_item_list() + if len(lil) == 0: + self.stdout.write(self.style.SUCCESS('No lost items. Nothing to delete.')) + else: + self.stdout.write(self.style.WARNING('Lost Items:')) + for item in lil: + self.stdout.write(self.style.WARNING(' - %s' % str(item))) + response = input('Delete the listed items and all data inside permanently? [y/N]: ') + if response.lower() == 'y': + self.stdout.write(self.style.SUCCESS('Deleting Items:')) + for item in lil: + item.delete() + self.stdout.write(self.style.SUCCESS(' - Deleted: %s' % str(item))) + + def lost_item_list(self): + lil = [] + for i in Item.objects.all(): + if not os.path.exists(pygal.get_full_path(i.rel_path)): + lil.append(i) + return lil diff --git a/management/commands/export_userdata.py b/management/commands/export_userdata.py new file mode 100644 index 0000000..c34d776 --- /dev/null +++ b/management/commands/export_userdata.py @@ -0,0 +1,37 @@ +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +import json +from pygal.models import Tag + +from ._private import KEY_USERDATA_VERSION +from ._private import KEY_USERDATA_FAVOURITE +from ._private import KEY_USERDATA_TAGS + + +class Command(BaseCommand): + help = 'Export userdata in JSON-Format.' + + def handle(self, *args, **options): + data = {} + data[KEY_USERDATA_VERSION] = 1 + for user in User.objects.all(): + favourites = user.favourite_of.all() + if len(favourites) > 0: + if KEY_USERDATA_FAVOURITE not in data: + data[KEY_USERDATA_FAVOURITE] = {} + data[KEY_USERDATA_FAVOURITE][user.username] = [i.rel_path for i in favourites] + for tag in Tag.objects.all(): + if KEY_USERDATA_TAGS not in data: + data[KEY_USERDATA_TAGS] = {} + if tag.item.rel_path not in data[KEY_USERDATA_TAGS]: + data[KEY_USERDATA_TAGS][tag.item.rel_path] = [] + tagdata = {} + tagdata['text'] = tag.text + if tag.has_valid_coordinates: + tagdata['topleft_x'] = tag.topleft_x + tagdata['topleft_y'] = tag.topleft_y + tagdata['bottomright_x'] = tag.bottomright_x + tagdata['bottomright_y'] = tag.bottomright_y + data[KEY_USERDATA_TAGS][tag.item.rel_path].append(tagdata) + + self.stdout.write(json.dumps(data, indent=4, sort_keys=True)) diff --git a/management/commands/import_userdata.py b/management/commands/import_userdata.py new file mode 100644 index 0000000..ed6b1f7 --- /dev/null +++ b/management/commands/import_userdata.py @@ -0,0 +1,98 @@ +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +import json +from pygal.models import Item, Tag +import os + +from ._private import KEY_USERDATA_VERSION +from ._private import KEY_USERDATA_FAVOURITE +from ._private import KEY_USERDATA_TAGS + + +class Command(BaseCommand): + help = 'Import userdata from a JSON-File.' + + def add_arguments(self, parser): + parser.add_argument('filename(s)', nargs='+', type=str) + + def error_skipped_file(self, path, reason): + self.stderr.write(' Skipped file %s caused by an error (%s) !' % (path, reason)) + + def handle(self, *args, **options): + for fn in options['filename(s)']: + fn = os.path.abspath(fn) + self.stdout.write('Parsing %s:' % fn) + try: + with open(fn, 'r') as fh: + data = json.load(fh) + try: + version = data.pop(KEY_USERDATA_VERSION) + except KeyError: + version = 1 + except PermissionError: + self.error_skipped_file(fn, 'file permission') + except json.decoder.JSONDecodeError: + self.error_skipped_file(fn, 'json decoding') + except FileNotFoundError: + self.error_skipped_file(fn, 'file not found') + else: + for section in data: + self.stdout.write(' Importing "%s":' % section) + cnt_all_datasets = 0 + cnt_success = 0 + cnt_already_exist = 0 + cnt_user_missing = 0 + cnt_item_missing = 0 + if section == KEY_USERDATA_FAVOURITE: + for username in data[section]: + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + user = None + for rel_path in data[section][username]: + cnt_all_datasets += 1 + if user is None: + cnt_user_missing += 1 + try: + i = Item.objects.get(rel_path=rel_path) + except Item.DoesNotExist: + cnt_item_missing += 1 + else: + if user in i.favourite_of.all(): + cnt_already_exist += 1 + else: + cnt_success += 1 + i.favourite_of.add(user) + elif section == KEY_USERDATA_TAGS: + for rel_path in data[section]: + cnt_all_datasets += len(data[section][rel_path]) + try: + i = Item.objects.get(rel_path=rel_path) + except Item.DoesNotExist: + cnt_item_missing += len(data[section][rel_path]) + else: + for tag_data in data[section][rel_path]: + if len(Tag.objects.filter(item=i, text=tag_data['text'])) > 0: + cnt_already_exist += 1 + else: + tag_data['item'] = i + Tag(**tag_data).save() + cnt_success += 1 + else: + self.stdout.write(self.style.ERROR(' Section unknown!')) + # + # REPORT + # + if cnt_all_datasets > 0: + if (cnt_success + cnt_already_exist) < cnt_all_datasets: + style = self.style.WARNING + self.stdout.write(style(' NOT all data imported.')) + else: + style = self.style.SUCCESS + self.stdout.write(style(' All data imported.')) + self.stdout.write(style(' %5d %s(s) successfully added.' % (cnt_success, section))) + self.stdout.write(style(' %5d %s(s) already set correctly.' % (cnt_already_exist, section))) + if cnt_user_missing > 0: + self.stdout.write(self.style.ERROR(' %5d %s(s) not added, because the target user does not exist.' % (cnt_user_missing, section))) + if cnt_item_missing > 0: + self.stdout.write(self.style.ERROR(' %5d %s(s) not added, because the target item does not exist.' % (cnt_item_missing, section))) diff --git a/management/commands/rebuild_cache.py b/management/commands/rebuild_cache.py new file mode 100644 index 0000000..6126d2e --- /dev/null +++ b/management/commands/rebuild_cache.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +import fstools +import pygal +from pygal.models import Item, TYPE_FOLDER, get_item_by_rel_path + + +class Command(BaseCommand): + def handle(self, *args, **options): + # rebuild existing non folder items + # + for item in Item.objects.all().exclude(type=TYPE_FOLDER): + item.__set_model_fields_from_file__() + item.save() + # rebuild folder items based on file system structure (not existing items will be created) + folder_list = fstools.dirlist(settings.ITEM_ROOT, rekursive=True) + folder_list.append(settings.ITEM_ROOT) + folder_list.sort(reverse=True) + for full_path in folder_list: + item = get_item_by_rel_path(pygal.get_rel_path(full_path)) + if item is not None: + item.__set_model_fields_from_file__() + item.save() + self.stdout.write(self.style.SUCCESS('Updated cached data of %d items.' % len(Item.objects.all().exclude(type=TYPE_FOLDER)))) + self.stdout.write(self.style.SUCCESS('Updated cached data of %d folders.' % len(Item.objects.filter(type=TYPE_FOLDER)))) diff --git a/management/commands/rebuild_index.py b/management/commands/rebuild_index.py new file mode 100644 index 0000000..0b5eb30 --- /dev/null +++ b/management/commands/rebuild_index.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand +from pygal.search import create_index, rebuild_index + + +class Command(BaseCommand): + def handle(self, *args, **options): + ix = create_index() + n = rebuild_index(ix) + self.stdout.write(self.style.SUCCESS('Search index for %d items created.') % n) diff --git a/migrations/0001_initial.py b/migrations/0001_initial.py new file mode 100644 index 0000000..546a28d --- /dev/null +++ b/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# Generated by Django 2.2.5 on 2019-12-20 12:09 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rel_path', models.TextField(unique=True)), + ('type', models.CharField(choices=[('audio', 'Audio'), ('folder', 'Folder'), ('image', 'Image'), ('other', 'Other'), ('video', 'Video')], max_length=25)), + ('public_access', models.BooleanField(default=False)), + ('uid_c', models.CharField(blank=True, max_length=50, null=True)), + ('settings_c', models.IntegerField(blank=True, null=True)), + ('data_version_c', models.IntegerField(blank=True, null=True)), + ('size_c', models.IntegerField(blank=True, null=True)), + ('datetime_c', models.DateTimeField(blank=True, null=True)), + ('exposure_program_c', models.CharField(blank=True, max_length=100, null=True)), + ('exposure_time_c', models.FloatField(blank=True, null=True)), + ('flash_c', models.CharField(blank=True, max_length=100, null=True)), + ('f_number_c', models.FloatField(blank=True, null=True)), + ('focal_length_c', models.FloatField(blank=True, null=True)), + ('lon_c', models.FloatField(blank=True, null=True)), + ('lat_c', models.FloatField(blank=True, null=True)), + ('height_c', models.IntegerField(blank=True, null=True)), + ('iso_c', models.IntegerField(blank=True, null=True)), + ('camera_vendor_c', models.CharField(blank=True, max_length=100, null=True)), + ('camera_model_c', models.CharField(blank=True, max_length=100, null=True)), + ('orientation_c', models.IntegerField(blank=True, null=True)), + ('width_c', models.IntegerField(blank=True, null=True)), + ('duration_c', models.FloatField(blank=True, null=True)), + ('ratio_c', models.FloatField(blank=True, null=True)), + ('album_c', models.CharField(blank=True, max_length=100, null=True)), + ('artist_c', models.CharField(blank=True, max_length=100, null=True)), + ('bitrate_c', models.IntegerField(blank=True, null=True)), + ('genre_c', models.CharField(blank=True, max_length=100, null=True)), + ('title_c', models.CharField(blank=True, max_length=100, null=True)), + ('track_c', models.IntegerField(blank=True, null=True)), + ('year_c', models.IntegerField(blank=True, null=True)), + ('num_audio_c', models.IntegerField(blank=True, null=True)), + ('num_folders_c', models.IntegerField(blank=True, null=True)), + ('num_images_c', models.IntegerField(blank=True, null=True)), + ('num_other_c', models.IntegerField(blank=True, null=True)), + ('num_videos_c', models.IntegerField(blank=True, null=True)), + ('sil_c', models.TextField(blank=True, null=True)), + ('favourite_of', models.ManyToManyField(blank=True, related_name='favourite_of', to=settings.AUTH_USER_MODEL)), + ('modify_access', models.ManyToManyField(blank=True, related_name='modify_access', to=settings.AUTH_USER_MODEL)), + ('read_access', models.ManyToManyField(blank=True, related_name='read_access', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Setting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('show_image', models.BooleanField(default=True)), + ('show_video', models.BooleanField(default=True)), + ('show_audio', models.BooleanField(default=False)), + ('show_other', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.CharField(max_length=100)), + ('topleft_x', models.IntegerField(blank=True, null=True)), + ('topleft_y', models.IntegerField(blank=True, null=True)), + ('bottomright_x', models.IntegerField(blank=True, null=True)), + ('bottomright_y', models.IntegerField(blank=True, null=True)), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pygal.Item')), + ], + ), + ] diff --git a/migrations/0002_setting_suspend_puplic.py b/migrations/0002_setting_suspend_puplic.py new file mode 100644 index 0000000..5d95dbb --- /dev/null +++ b/migrations/0002_setting_suspend_puplic.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.5 on 2019-12-22 08:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pygal', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='setting', + name='suspend_puplic', + field=models.BooleanField(default=True), + ), + ] diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models.py b/models.py new file mode 100644 index 0000000..be5c181 --- /dev/null +++ b/models.py @@ -0,0 +1,642 @@ +import datetime +from django.contrib.auth.models import User +from django.db import models +from django.utils import formats, timezone +from django.urls import reverse +import fstools +import json +import logging +import os +from PIL import Image +import pygal +import subprocess +import time + +DEBUG = False + + +TYPE_FOLDER = 'folder' +TYPE_IMAGE = 'image' +TYPE_VIDEO = 'video' +TYPE_AUDIO = 'audio' +TYPE_OTHER = 'other' + +EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ] +EXTENTIONS_AUDIO = ['.mp3', ] +EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ] + + +# Get a logger instance +logger = logging.getLogger("CACHING") + + +def get_item_type(full_path): + if os.path.isdir(full_path): + return TYPE_FOLDER + else: + if os.path.splitext(full_path)[1].lower() in EXTENTIONS_IMAGE: + return TYPE_IMAGE + elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_VIDEO: + return TYPE_VIDEO + elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_AUDIO: + return TYPE_AUDIO + return TYPE_OTHER + + +def supported_types(): + rv = [] + if pygal.show_audio(): + rv.append(TYPE_AUDIO) + if pygal.show_image(): + rv.append(TYPE_IMAGE) + if pygal.show_other(): + rv.append(TYPE_OTHER) + if pygal.show_video(): + rv.append(TYPE_VIDEO) + return rv + + +def get_item_by_rel_path(rel_path): + try: + rv = Item.objects.get(rel_path=rel_path) + except Item.DoesNotExist: + rv = None + if rv is not None: + # return the existing item + return rv + else: + # create new item, if rel_path exists in filesystem (folders needs to hold files) + full_path = pygal.get_full_path(rel_path) + if os.path.exists(full_path): + # file exists or folder has files in substructure + if get_item_type(full_path) != TYPE_FOLDER or len(fstools.filelist(full_path)) > 0: + i = Item(rel_path=rel_path) + i.save() + logger.info('New Item created: %s', repr(rel_path)) + return i + + +def ffprobe_lines(full_path): + def _ffprobe_command(full_path): + return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path] + + def _avprobe_command(full_path): + return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path] + + def decode(string): + for i in ['utf-8', 'cp1252']: + try: + return string.decode(i) + except UnicodeEncodeError: + pass + except UnicodeDecodeError: + pass + return string + + try: + try: + text_to_be_parsed = subprocess.check_output(_avprobe_command(full_path)) + except OSError: + text_to_be_parsed = subprocess.check_output(_ffprobe_command(full_path)) + except subprocess.CalledProcessError: + text_to_be_parsed = '' + return decode(text_to_be_parsed).splitlines() + + +def is_valid_area(x1, y1, x2, y2): + for p in [x1, y1, x2, y2]: + if type(p) is not int: + return False + if (x1, y1) == (x2, y2): + return False + return True + + +class ItemData(dict): + DATA_FIELDS = [ + 'size', + 'datetime', + 'exposure_program', + 'exposure_time', + 'flash', + 'f_number', + 'focal_length', + 'lon', + 'lat', + 'height', + 'iso', + 'camera_vendor', + 'camera_model', + 'orientation', + 'width', + 'duration', + 'ratio', + 'album', + 'artist', + 'bitrate', + 'genre', + 'title', + 'track', + 'year', + 'sil', + 'num_audio', + 'num_folders', + 'num_images', + 'num_other', + 'num_videos', + ] + DEFAULT_VALUES = { + 'size': 0, + 'datetime': datetime.datetime(1900, 1, 1), + 'exposure_program': '-', + 'exposure_time': 0, + 'flash': '-', + 'f_number': 0, + 'focal_length': 0, + 'iso': 0, + 'camera_vendor': '-', + 'camera_model': '-', + 'orientation': 1, + 'duration': 3, + 'ratio': 1, + 'album': '-', + 'artist': '-', + 'bitrate': 0, + 'genre': '-', + 'title': '-', + 'track': 0, + 'year': 0, + 'num_audio': 0, + 'num_folders': 0, + 'num_images': 0, + 'num_other': 0, + 'num_videos': 0, + } + + def __init__(self, item): + for key in self.DATA_FIELDS: + value = getattr(item, key + '_c') + if value is not None: + self[key] = value + setattr(self, key, value) + else: + if key in self.DEFAULT_VALUES: + setattr(self, key, self.DEFAULT_VALUES[key]) + + @property + def formatted_datetime(self): + try: + return formats.date_format(self.get('datetime'), format="SHORT_DATE_FORMAT", use_l10n=True) + ' - ' + formats.time_format(self.get('datetime'), use_l10n=True) + except AttributeError: + return 'No Datetime available' + + @property + def gps(self): + if self.get('lon') and self.get('lat'): + return {'lon': self.get('lon'), 'lat': self.get('lat')} + else: + return None + + +class Item(models.Model): + DATA_VERSION_NUMBER = 0 + # + rel_path = models.TextField(unique=True) + type = models.CharField(max_length=25, choices=((TYPE_AUDIO, 'Audio'), (TYPE_FOLDER, 'Folder'), (TYPE_IMAGE, 'Image'), (TYPE_OTHER, 'Other'), (TYPE_VIDEO, 'Video'))) + public_access = models.BooleanField(default=False) + read_access = models.ManyToManyField(User, related_name="read_access", blank=True) + modify_access = models.ManyToManyField(User, related_name="modify_access", blank=True) + favourite_of = models.ManyToManyField(User, related_name="favourite_of", blank=True) + # + uid_c = models.CharField(max_length=50, null=True, blank=True) + settings_c = models.IntegerField(null=True, blank=True) + data_version_c = models.IntegerField(null=True, blank=True) + # common + size_c = models.IntegerField(null=True, blank=True) + datetime_c = models.DateTimeField(null=True, blank=True) + # image + exposure_program_c = models.CharField(max_length=100, null=True, blank=True) + exposure_time_c = models.FloatField(null=True, blank=True) + flash_c = models.CharField(max_length=100, null=True, blank=True) + f_number_c = models.FloatField(null=True, blank=True) + focal_length_c = models.FloatField(null=True, blank=True) + lon_c = models.FloatField(null=True, blank=True) + lat_c = models.FloatField(null=True, blank=True) + height_c = models.IntegerField(null=True, blank=True) + iso_c = models.IntegerField(null=True, blank=True) + camera_vendor_c = models.CharField(max_length=100, null=True, blank=True) + camera_model_c = models.CharField(max_length=100, null=True, blank=True) + orientation_c = models.IntegerField(null=True, blank=True) + width_c = models.IntegerField(null=True, blank=True) + # video + duration_c = models.FloatField(null=True, blank=True) + ratio_c = models.FloatField(null=True, blank=True) + # audio + album_c = models.CharField(max_length=100, null=True, blank=True) + artist_c = models.CharField(max_length=100, null=True, blank=True) + bitrate_c = models.IntegerField(null=True, blank=True) + genre_c = models.CharField(max_length=100, null=True, blank=True) + title_c = models.CharField(max_length=100, null=True, blank=True) + track_c = models.IntegerField(null=True, blank=True) + year_c = models.IntegerField(null=True, blank=True) + # folder + num_audio_c = models.IntegerField(null=True, blank=True) + num_folders_c = models.IntegerField(null=True, blank=True) + num_images_c = models.IntegerField(null=True, blank=True) + num_other_c = models.IntegerField(null=True, blank=True) + num_videos_c = models.IntegerField(null=True, blank=True) + sil_c = models.TextField(null=True, blank=True) + + def __init__(self, *args, **kwargs): + self.__current_uid__ = None + self.__current_settings__ = None + models.Model.__init__(self, *args, **kwargs) + + @property + def name(self): + return os.path.splitext(os.path.basename(self.rel_path))[0] + + @property + def item_data(self): + if self.__cache_update_needed__(): + # + self.__set_model_fields_from_file__() + # + self.save() + return self.cached_item_data + + @property + def cached_item_data(self): + return ItemData(self) + + def get_admin_url(self): + """the url to the Django admin interface for the model instance""" + info = (self._meta.app_label, self._meta.model_name) + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + + def suspended(self, user): + if pygal.suspend_public() and not user.is_authenticated: + return True + + def may_read(self, user): + if self.suspended(user): + return False + elif self.type == TYPE_FOLDER: + return True + else: + parent = get_item_by_rel_path(os.path.dirname(self.rel_path)) + if parent.public_access is True: + return True + if user is None: + return False + if not user.is_authenticated: + return False + if user.is_superuser: + return True + return user in parent.read_access.all() + + def may_modify(self, user): + if self.suspended(user): + return False + elif self.type == TYPE_FOLDER: + return user in self.modify_access.all() + else: + if user is None: + return False + if not user.is_authenticated: + return False + if user.is_superuser: + return True + parent = get_item_by_rel_path(os.path.dirname(self.rel_path)) + return user in parent.modify_access.all() + + def sort_string(self): + try: + tm = int(self.item_data.datetime.strftime('%s')) + except AttributeError: + raise AttributeError('Unable to create a sortstring for %s. Used datetime was: %s' % (str(self), repr(self.item_data.datetime))) + return '%012d_%s' % (tm, os.path.basename(self.rel_path)) + + def sorted_itemlist(self): + if self.type == TYPE_FOLDER: + rv = [] + for rel_path in json.loads(self.item_data['sil']): + try: + rv.append(Item.objects.get(rel_path=rel_path)) + except Item.DoesNotExist: + raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), rel_path)) + return rv + else: + return None + + def thumbnail_item(self): + if self.type == TYPE_FOLDER: + try: + first_rel_path = json.loads(self.item_data['sil'])[0] + return Item.objects.get(rel_path=first_rel_path).thumbnail_item() + except IndexError: + raise Item.DoesNotExist("%s: Tried to get the thumbnail_item for an empty list." % str(self)) + except Item.DoesNotExist: + raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), first_rel_path)) + else: + return self + + def all_itemslist(self): + rv = [] + for i in self.sorted_itemlist(): + if i.type != TYPE_FOLDER: + rv.append(i) + else: + rv.extend(i.all_itemslist()) + return rv + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.__cache_update_needed__(): + self.__set_model_fields_from_file__() + return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) + + def current_uid(self): + if self.__current_uid__ is None: + self.__current_uid__ = fstools.uid(pygal.get_full_path(self.rel_path), None) + return self.__current_uid__ + + def current_settings(self): + if self.__current_settings__ is None: + self.__current_settings__ = pygal.show_audio() * 1 + pygal.show_image() * 2 + pygal.show_other() * 3 + pygal.show_video() * 4 + return self.__current_settings__ + + def __cache_update_needed__(self): + if self.type == TYPE_FOLDER: + return DEBUG or self.settings_c != self.current_settings() or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER + else: + return DEBUG or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER + + def __set_model_fields_from_file__(self): + logger.info('Updating cached data for Item: %s' % repr(self.rel_path)) + full_path = pygal.get_full_path(self.rel_path) + self.type = get_item_type(full_path) + self.uid_c = self.current_uid() + self.settings_c = self.current_settings() + self.data_version_c = self.DATA_VERSION_NUMBER + if self.type == TYPE_AUDIO: + self.__update_audio_file_data__(full_path) + elif self.type == TYPE_FOLDER: + self.__update_folder_file_data__(full_path) + elif self.type == TYPE_IMAGE: + self.__update_image_file_data__(full_path) + elif self.type == TYPE_OTHER: + self.__update_other_file_data__(full_path) + elif self.type == TYPE_VIDEO: + self.__update_video_file_data__(full_path) + for key, value in self.cached_item_data.items(): + logger.debug(' - Adding %s=%s', key, repr(value)) + + def __update_audio_file_data__(self, full_path): + self.size_c = os.path.getsize(full_path) + # + tag_type_target_dict = {} + tag_type_target_dict['album'] = (str, 'album') + tag_type_target_dict['TAG:album'] = (str, 'album') + tag_type_target_dict['TAG:artist'] = (str, 'artist') + tag_type_target_dict['artist'] = (str, 'artist') + tag_type_target_dict['bit_rate'] = (self.__int_conv__, 'bitrate') + tag_type_target_dict['duration'] = (float, 'duration') + tag_type_target_dict['TAG:genre'] = (str, 'genre') + tag_type_target_dict['genre'] = (str, 'genre') + tag_type_target_dict['TAG:title'] = (str, 'title') + tag_type_target_dict['title'] = (str, 'title') + tag_type_target_dict['TAG:track'] = (self.__int_conv__, 'track') + tag_type_target_dict['track'] = (self.__int_conv__, 'track') + tag_type_target_dict['TAG:date'] = (self.__int_conv__, 'year') + tag_type_target_dict['date'] = (self.__int_conv__, 'year') + for line in ffprobe_lines(full_path): + try: + key, val = [snippet.strip() for snippet in line.split('=')] + except ValueError: + continue + else: + if key in tag_type_target_dict: + tp, name = tag_type_target_dict[key] + try: + setattr(self, name + '_c', tp(val)) + except ValueError: + logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name) + # + if self.year_c is not None and self.track_c is not None: + self.datetime_c = datetime.datetime(max(1971, self.year_c), 1, 1, 12, 0, (60 - self.track_c) % 60, tzinfo=timezone.utc) + + def __update_folder_file_data__(self, full_path): + sil = [] + self.size_c = 0 + self.num_audio_c = 0 + self.num_folders_c = 1 + self.num_images_c = 0 + self.num_other_c = 0 + self.num_videos_c = 0 + for fn in os.listdir(full_path): + sub_rel_path = pygal.get_rel_path(os.path.join(full_path, fn)) + sub_item = get_item_by_rel_path(sub_rel_path) + # size, num_* + if sub_item is not None: # Item does really exist / has sub-items + if (sub_item.type == TYPE_FOLDER and len(json.loads(sub_item.item_data['sil']))) or sub_item.type in supported_types(): + self.size_c += sub_item.item_data.size + if sub_item.type == TYPE_AUDIO: + self.num_audio_c += 1 + elif sub_item.type == TYPE_FOLDER: + self.num_audio_c += sub_item.item_data['num_audio'] + self.num_folders_c += sub_item.item_data['num_folders'] + self.num_images_c += sub_item.item_data['num_images'] + self.num_other_c += sub_item.item_data['num_other'] + self.num_videos_c += sub_item.item_data['num_videos'] + elif sub_item.type == TYPE_IMAGE: + self.num_images_c += 1 + elif sub_item.type == TYPE_OTHER: + self.num_other_c += 1 + elif sub_item.type == TYPE_VIDEO: + self.num_videos_c += 1 + # sorted item list + sil.append(sub_item) + sil.sort(key=lambda entry: entry.sort_string(), reverse=True) + self.sil_c = json.dumps([i.rel_path for i in sil], indent=4) + # datetime + if len(sil) > 0: + self.datetime_c = sil[0].datetime_c + + def __update_image_file_data__(self, full_path): + self.size_c = os.path.getsize(full_path) + # + tag_type_target_dict = {} + tag_type_target_dict[0x9003] = (self.__datetime_conv__, 'datetime') + tag_type_target_dict[0x8822] = (self.__exposure_program_conv__, 'exposure_program') + tag_type_target_dict[0x829A] = (self.__num_denum_conv__, 'exposure_time') + tag_type_target_dict[0x9209] = (self.__flash_conv__, 'flash') + tag_type_target_dict[0x829D] = (self.__num_denum_conv__, 'f_number') + tag_type_target_dict[0x920A] = (self.__num_denum_conv__, 'focal_length') + tag_type_target_dict[0x8825] = (self.__gps_conv__, ('lon', 'lat')) + tag_type_target_dict[0xA003] = (self.__int_conv__, 'height') + tag_type_target_dict[0x8827] = (self.__int_conv__, 'iso') + tag_type_target_dict[0x010F] = (str, 'camera_vendor') + tag_type_target_dict[0x0110] = (str, 'camera_model') + tag_type_target_dict[0x0112] = (self.__int_conv__, 'orientation') + tag_type_target_dict[0xA002] = (self.__int_conv__, 'width') + im = Image.open(full_path) + try: + exif = dict(im._getexif().items()) + except AttributeError: + logger.debug('%s does not have any exif information', full_path) + else: + for key in tag_type_target_dict: + if key in exif: + tp, name = tag_type_target_dict[key] + if type(name) is tuple: + data = tp(exif[key]) or (None, None) + for name, val in zip(name, data): + setattr(self, name + '_c', val) + else: + setattr(self, name + '_c', tp(exif[key])) + + def __update_other_file_data__(self, full_path): + self.size_c = os.path.getsize(full_path) + # + self.datetime_c = datetime.datetime.fromtimestamp(os.path.getctime(full_path), tz=timezone.utc) + + def __update_video_file_data__(self, full_path): + self.size_c = os.path.getsize(full_path) + # + tag_type_target_dict = {} + tag_type_target_dict['creation_time'] = (self.__vid_datetime_conv__, 'datetime') + tag_type_target_dict['TAG:creation_time'] = (self.__vid_datetime_conv__, 'datetime') + tag_type_target_dict['duration'] = (float, 'duration') + tag_type_target_dict['height'] = (self.__int_conv__, 'height') + tag_type_target_dict['width'] = (self.__int_conv__, 'width') + tag_type_target_dict['display_aspect_ratio'] = (self.__ratio_conv__, 'ratio') + for line in ffprobe_lines(full_path): + try: + key, val = [snippet.strip() for snippet in line.split('=')] + except ValueError: + continue + else: + if key in tag_type_target_dict: + tp, name = tag_type_target_dict[key] + try: + setattr(self, name + '_c', tp(val)) + except ValueError: + logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name) + + def __datetime_conv__(self, dt): + format_string = "%Y:%m:%d %H:%M:%S%z" + return datetime.datetime.strptime(dt + '+0000', format_string) + + def __exposure_program_conv__(self, n): + return { + 0: 'Unidentified', + 1: 'Manual', + 2: 'Program Normal', + 3: 'Aperture Priority', + 4: 'Shutter Priority', + 5: 'Program Creative', + 6: 'Program Action', + 7: 'Portrait Mode', + 8: 'Landscape Mode' + }.get(n, '-') + + def __flash_conv__(self, n): + return { + 0: 'No', + 1: 'Fired', + 5: 'Fired (?)', # no return sensed + 7: 'Fired (!)', # return sensed + 9: 'Fill Fired', + 13: 'Fill Fired (?)', + 15: 'Fill Fired (!)', + 16: 'Off', + 24: 'Auto Off', + 25: 'Auto Fired', + 29: 'Auto Fired (?)', + 31: 'Auto Fired (!)', + 32: 'Not Available' + }.get(n, '-') + + def __int_conv__(self, value): + try: + return int(value) + except ValueError: + for c in ['.', '/', '-']: + p = value.find(c) + if p >= 0: + value = value[:p] + if value == '': + return 0 + return int(value) + + def __num_denum_conv__(self, data): + num, denum = data + return num / denum + + def __gps_conv__(self, data): + def lat_lon_cal(lon_or_lat): + lon_lat = 0. + fac = 1. + for num, denum in lon_or_lat: + lon_lat += float(num) / float(denum) * fac + fac *= 1. / 60. + return lon_lat + try: + lon = lat_lon_cal(data[0x0004]) + lat = lat_lon_cal(data[0x0002]) + if lon != 0 or lat != 0: # do not use lon and lat equal 0, caused by motorola gps weakness + return lon, lat + except KeyError: + logger.warning('GPS data extraction failed for %s: %s', self.name, repr(data)) + return None + + def __vid_datetime_conv__(self, dt): + try: + dt = dt[:dt.index('.')] + except ValueError: + pass # time string seems to have no '.' + dt = dt.replace('T', ' ').replace('/', '').replace('\\', '') + if len(dt) == 16: + dt += ':00' + dt += '+0000' + format_string = '%Y-%m-%d %H:%M:%S%z' + return datetime.datetime.strptime(dt, format_string) + + def __ratio_conv__(self, ratio): + ratio = ratio.replace('\\', '') + num, denum = ratio.split(':') + return float(num) / float(denum) + + def __str__(self): + return 'Item: %s' % self.rel_path + + +class Tag(models.Model): + item = models.ForeignKey(Item, on_delete=models.CASCADE) + text = models.CharField(max_length=100) + topleft_x = models.IntegerField(null=True, blank=True) + topleft_y = models.IntegerField(null=True, blank=True) + bottomright_x = models.IntegerField(null=True, blank=True) + bottomright_y = models.IntegerField(null=True, blank=True) + + def __init__(self, *args, **kwargs): + self.__tm_start__ = time.time() + models.Model.__init__(self, *args, **kwargs) + logger.log(5, 'Initialising Tag Model object in %.02fs: %s', time.time() - self.__tm_start__, str(self)) + + @property + def has_valid_coordinates(self): + return is_valid_area(self.topleft_x, self.topleft_y, self.bottomright_x, self.bottomright_y) + + def get_admin_url(self): + """the url to the Django admin interface for the model instance""" + info = (self._meta.app_label, self._meta.model_name) + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + + +class Setting(models.Model): + suspend_puplic = models.BooleanField(default=True) + show_image = models.BooleanField(default=True) + show_video = models.BooleanField(default=True) + show_audio = models.BooleanField(default=False) + show_other = models.BooleanField(default=False) diff --git a/queries.py b/queries.py new file mode 100644 index 0000000..06b28ca --- /dev/null +++ b/queries.py @@ -0,0 +1,20 @@ +from .models import Item, supported_types +import pygal +from .search import load_index, search + + +def get_readable_item_query(request, item_query): + uids = [] + for i in item_query: + if i.may_read(request.user): + if i.type in supported_types(): + uids.append(i.id) + uids = set(uids) + return Item.objects.filter(id__in=uids) + + +def search_result_query(request): + search_txt = pygal.get_search_query(request) + ix = load_index() + # + return get_readable_item_query(request, search(ix, search_txt)) diff --git a/search.py b/search.py new file mode 100644 index 0000000..533deda --- /dev/null +++ b/search.py @@ -0,0 +1,125 @@ +from django.conf import settings +import fstools +import logging +from .models import Item +import os +from whoosh.fields import Schema, ID, TEXT, KEYWORD, NUMERIC, DATETIME +from whoosh.qparser.dateparse import DateParserPlugin +from whoosh import index, qparser +from pygal.models import TYPE_FOLDER + +logger = logging.getLogger("WHOOSH") + + +SCHEMA = Schema( + rel_path=ID(unique=True, stored=True), + # Item + name=TEXT, + type=TEXT, + favourite_of=KEYWORD, + datetime=DATETIME, + size=NUMERIC, + # Tag + tag=KEYWORD, + # Image Cache + exposure_program=TEXT, + exposure_time=NUMERIC, + flash=TEXT, + f_number=NUMERIC, + focal_length=NUMERIC, + lon=NUMERIC, + lat=NUMERIC, + height=NUMERIC, + iso=NUMERIC, + camera_vendor=TEXT, + camera_model=TEXT, + orientation=NUMERIC, + width=NUMERIC, + # Audio Cache + album=TEXT, + artist=TEXT, + bitrate=NUMERIC, + duration=NUMERIC, + genre=TEXT, + title=TEXT, + track=NUMERIC, + year=NUMERIC, + # + ratio=NUMERIC, +) + + +def create_index(): + logger.debug('Search Index created.') + return index.create_in(settings.WHOOSH_PATH, schema=SCHEMA) + + +def load_index(): + if not os.path.exists(settings.WHOOSH_PATH): + fstools.mkdir(settings.WHOOSH_PATH) + try: + ix = index.open_dir(settings.WHOOSH_PATH) + except index.EmptyIndexError: + ix = create_index() + else: + logger.debug('Search Index opened.') + return ix + + +def item_is_supported(item): + return item.type != TYPE_FOLDER + + +def add_item(ix, item): + # Collect data for the item + # + data = { + 'rel_path': item.rel_path, + 'name': os.path.splitext(item.rel_path.split('/')[-1])[0], + 'type': item.type, + } + favourite_of = item.favourite_of.all() + if len(favourite_of) > 0: + data['favourite_of'] = ' '.join([u.username for u in favourite_of]) + tags = item.tag_set.all() + if len(tags) > 0: + data['tag'] = ' '.join([t.text for t in tags]) + for key, value in item.cached_item_data.items(): + data[key] = value + # Write data to the index + # + with ix.writer() as w: + logger.info('Adding document with rel_path=%s to the search index.', data.get('rel_path')) + w.add_document(**data) + for key in data: + logger.debug(' - Adding %s=%s', key, repr(data[key])) + + +def delete_item(ix, item): + with ix.writer() as w: + logger.info('Removing document with rel_path=%s from the search index.', item.rel_path) + w.delete_by_term("rel_path", item.rel_path) + + +def update_item(ix, item): + if item_is_supported(item): + delete_item(ix, item) + add_item(ix, item) + + +def rebuild_index(ix): + for item in Item.objects.all().exclude(type=TYPE_FOLDER): + add_item(ix, item) + return len(Item.objects.all().exclude(type=TYPE_FOLDER)) + + +def search(ix, search_txt): + qp = qparser.MultifieldParser(['name', 'tag'], ix.schema) + qp.add_plugin(DateParserPlugin(free=True)) + q = qp.parse(search_txt) + with ix.searcher() as s: + results = s.search(q, limit=None) + rpl = [] + for hit in results: + rpl.append(hit['rel_path']) + return Item.objects.filter(rel_path__in=rpl) diff --git a/signals.py b/signals.py new file mode 100644 index 0000000..fa48a72 --- /dev/null +++ b/signals.py @@ -0,0 +1,37 @@ +from django.db.models.signals import post_save +from django.db.models.signals import post_delete +from django.dispatch import receiver +import logging +from .models import Item, Tag +from .search import load_index, delete_item, update_item +import shutil +from .views.image import base_item + +# Get a logger instance +clogger = logging.getLogger("CACHING") + +__pre_remove__ = None +__pre_add__ = None + + +@receiver(post_delete, sender=Item) +def item_delete(instance, **kwargs): + # delete cached xnails + clogger.info('Deleting Xnails stored in "%s".', base_item.__cache_image_folder__(None, instance.rel_path)) + shutil.rmtree(base_item.__cache_image_folder__(None, instance.rel_path), True) + # delete index entry + ix = load_index() + delete_item(ix, instance) + + +@receiver(post_save, sender=Item) +def item_save(instance, **kwargs): + ix = load_index() + update_item(ix, instance) + + +@receiver(post_save, sender=Tag) +@receiver(post_delete, sender=Tag) +def tag_change(instance, **kwargs): + ix = load_index() + update_item(ix, instance.item) diff --git a/static/pygal/js/imgareaselect-js/css/imgareaselect-animated.css b/static/pygal/js/imgareaselect-js/css/imgareaselect-animated.css new file mode 100644 index 0000000..6ec6e10 --- /dev/null +++ b/static/pygal/js/imgareaselect-js/css/imgareaselect-animated.css @@ -0,0 +1,41 @@ +/* + * imgAreaSelect animated border style + */ + +.imgareaselect-border1 { + background: url(border-anim-v.gif) repeat-y left top; +} + +.imgareaselect-border2 { + background: url(border-anim-h.gif) repeat-x left top; +} + +.imgareaselect-border3 { + background: url(border-anim-v.gif) repeat-y right top; +} + +.imgareaselect-border4 { + background: url(border-anim-h.gif) repeat-x left bottom; +} + +.imgareaselect-border1, .imgareaselect-border2, +.imgareaselect-border3, .imgareaselect-border4 { + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-handle { + background-color: #fff; + border: solid 1px #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-outer { + background-color: #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-selection { +} \ No newline at end of file diff --git a/static/pygal/js/imgareaselect-js/css/imgareaselect-default.css b/static/pygal/js/imgareaselect-js/css/imgareaselect-default.css new file mode 100644 index 0000000..f4fe341 --- /dev/null +++ b/static/pygal/js/imgareaselect-js/css/imgareaselect-default.css @@ -0,0 +1,41 @@ +/* + * imgAreaSelect default style + */ + +.imgareaselect-border1 { + background: url(border-v.gif) repeat-y left top; +} + +.imgareaselect-border2 { + background: url(border-h.gif) repeat-x left top; +} + +.imgareaselect-border3 { + background: url(border-v.gif) repeat-y right top; +} + +.imgareaselect-border4 { + background: url(border-h.gif) repeat-x left bottom; +} + +.imgareaselect-border1, .imgareaselect-border2, +.imgareaselect-border3, .imgareaselect-border4 { + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-handle { + background-color: #fff; + border: solid 1px #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-outer { + background-color: #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-selection { +} \ No newline at end of file diff --git a/static/pygal/js/imgareaselect-js/css/imgareaselect-deprecated.css b/static/pygal/js/imgareaselect-js/css/imgareaselect-deprecated.css new file mode 100644 index 0000000..eaf0ddc --- /dev/null +++ b/static/pygal/js/imgareaselect-js/css/imgareaselect-deprecated.css @@ -0,0 +1,36 @@ +/* + * imgAreaSelect style to be used with deprecated options + */ + +.imgareaselect-border1, .imgareaselect-border2, +.imgareaselect-border3, .imgareaselect-border4 { + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-border1 { + border: solid 1px #000; +} + +.imgareaselect-border2 { + border: dashed 1px #fff; +} + +.imgareaselect-handle { + background-color: #fff; + border: solid 1px #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-outer { + background-color: #000; + filter: alpha(opacity=40); + opacity: 0.4; +} + +.imgareaselect-selection { + background-color: #fff; + filter: alpha(opacity=0); + opacity: 0; +} diff --git a/static/pygal/js/imgareaselect-js/jquery.imgareaselect.js b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.js new file mode 100644 index 0000000..7e1b790 --- /dev/null +++ b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.js @@ -0,0 +1,730 @@ +/* + * imgAreaSelect jQuery plugin + * version 0.9.10 + * + * Copyright (c) 2008-2013 Michal Wojciechowski (odyniec.net) + * + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://odyniec.net/projects/imgareaselect/ + * + */ + +(function($) { + +var abs = Math.abs, + max = Math.max, + min = Math.min, + round = Math.round; + +function div() { + return $('
'); +} + +$.imgAreaSelect = function (img, options) { + var + + $img = $(img), + + imgLoaded, + + $box = div(), + $area = div(), + $border = div().add(div()).add(div()).add(div()), + $outer = div().add(div()).add(div()).add(div()), + $handles = $([]), + + $areaOpera, + + left, top, + + imgOfs = { left: 0, top: 0 }, + + imgWidth, imgHeight, + + $parent, + + parOfs = { left: 0, top: 0 }, + + zIndex = 0, + + position = 'absolute', + + startX, startY, + + scaleX, scaleY, + + resize, + + minWidth, minHeight, maxWidth, maxHeight, + + aspectRatio, + + shown, + + x1, y1, x2, y2, + + selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 }, + + docElem = document.documentElement, + + ua = navigator.userAgent, + + $p, d, i, o, w, h, adjusted; + + function viewX(x) { + return x + imgOfs.left - parOfs.left; + } + + function viewY(y) { + return y + imgOfs.top - parOfs.top; + } + + function selX(x) { + return x - imgOfs.left + parOfs.left; + } + + function selY(y) { + return y - imgOfs.top + parOfs.top; + } + + function evX(event) { + return event.pageX - parOfs.left; + } + + function evY(event) { + return event.pageY - parOfs.top; + } + + function getSelection(noScale) { + var sx = noScale || scaleX, sy = noScale || scaleY; + + return { x1: round(selection.x1 * sx), + y1: round(selection.y1 * sy), + x2: round(selection.x2 * sx), + y2: round(selection.y2 * sy), + width: round(selection.x2 * sx) - round(selection.x1 * sx), + height: round(selection.y2 * sy) - round(selection.y1 * sy) }; + } + + function setSelection(x1, y1, x2, y2, noScale) { + var sx = noScale || scaleX, sy = noScale || scaleY; + + selection = { + x1: round(x1 / sx || 0), + y1: round(y1 / sy || 0), + x2: round(x2 / sx || 0), + y2: round(y2 / sy || 0) + }; + + selection.width = selection.x2 - selection.x1; + selection.height = selection.y2 - selection.y1; + } + + function adjust() { + if (!imgLoaded || !$img.width()) + return; + + imgOfs = { left: round($img.offset().left), top: round($img.offset().top) }; + + imgWidth = $img.innerWidth(); + imgHeight = $img.innerHeight(); + + imgOfs.top += ($img.outerHeight() - imgHeight) >> 1; + imgOfs.left += ($img.outerWidth() - imgWidth) >> 1; + + minWidth = round(options.minWidth / scaleX) || 0; + minHeight = round(options.minHeight / scaleY) || 0; + maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth)); + maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight)); + + if ($().jquery == '1.3.2' && position == 'fixed' && + !docElem['getBoundingClientRect']) + { + imgOfs.top += max(document.body.scrollTop, docElem.scrollTop); + imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft); + } + + parOfs = /absolute|relative/.test($parent.css('position')) ? + { left: round($parent.offset().left) - $parent.scrollLeft(), + top: round($parent.offset().top) - $parent.scrollTop() } : + position == 'fixed' ? + { left: $(document).scrollLeft(), top: $(document).scrollTop() } : + { left: 0, top: 0 }; + + left = viewX(0); + top = viewY(0); + + if (selection.x2 > imgWidth || selection.y2 > imgHeight) + doResize(); + } + + function update(resetKeyPress) { + if (!shown) return; + + $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) }) + .add($area).width(w = selection.width).height(h = selection.height); + + $area.add($border).add($handles).css({ left: 0, top: 0 }); + + $border + .width(max(w - $border.outerWidth() + $border.innerWidth(), 0)) + .height(max(h - $border.outerHeight() + $border.innerHeight(), 0)); + + $($outer[0]).css({ left: left, top: top, + width: selection.x1, height: imgHeight }); + $($outer[1]).css({ left: left + selection.x1, top: top, + width: w, height: selection.y1 }); + $($outer[2]).css({ left: left + selection.x2, top: top, + width: imgWidth - selection.x2, height: imgHeight }); + $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2, + width: w, height: imgHeight - selection.y2 }); + + w -= $handles.outerWidth(); + h -= $handles.outerHeight(); + + switch ($handles.length) { + case 8: + $($handles[4]).css({ left: w >> 1 }); + $($handles[5]).css({ left: w, top: h >> 1 }); + $($handles[6]).css({ left: w >> 1, top: h }); + $($handles[7]).css({ top: h >> 1 }); + case 4: + $handles.slice(1,3).css({ left: w }); + $handles.slice(2,4).css({ top: h }); + } + + if (resetKeyPress !== false) { + if ($.imgAreaSelect.onKeyPress != docKeyPress) + $(document).unbind($.imgAreaSelect.keyPress, + $.imgAreaSelect.onKeyPress); + + if (options.keys) + $(document)[$.imgAreaSelect.keyPress]( + $.imgAreaSelect.onKeyPress = docKeyPress); + } + + if (msie && $border.outerWidth() - $border.innerWidth() == 2) { + $border.css('margin', 0); + setTimeout(function () { $border.css('margin', 'auto'); }, 0); + } + } + + function doUpdate(resetKeyPress) { + adjust(); + update(resetKeyPress); + x1 = viewX(selection.x1); y1 = viewY(selection.y1); + x2 = viewX(selection.x2); y2 = viewY(selection.y2); + } + + function hide($elem, fn) { + options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide(); + + } + + function areaMouseMove(event) { + var x = selX(evX(event)) - selection.x1, + y = selY(evY(event)) - selection.y1; + + if (!adjusted) { + adjust(); + adjusted = true; + + $box.one('mouseout', function () { adjusted = false; }); + } + + resize = ''; + + if (options.resizable) { + if (y <= options.resizeMargin) + resize = 'n'; + else if (y >= selection.height - options.resizeMargin) + resize = 's'; + if (x <= options.resizeMargin) + resize += 'w'; + else if (x >= selection.width - options.resizeMargin) + resize += 'e'; + } + + $box.css('cursor', resize ? resize + '-resize' : + options.movable ? 'move' : ''); + if ($areaOpera) + $areaOpera.toggle(); + } + + function docMouseUp(event) { + $('body').css('cursor', ''); + if (options.autoHide || selection.width * selection.height == 0) + hide($box.add($outer), function () { $(this).hide(); }); + + $(document).unbind('mousemove', selectingMouseMove); + $box.mousemove(areaMouseMove); + + options.onSelectEnd(img, getSelection()); + } + + function areaMouseDown(event) { + if (event.which != 1) return false; + + adjust(); + + if (resize) { + $('body').css('cursor', resize + '-resize'); + + x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']); + y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']); + + $(document).mousemove(selectingMouseMove) + .one('mouseup', docMouseUp); + $box.unbind('mousemove', areaMouseMove); + } + else if (options.movable) { + startX = left + selection.x1 - evX(event); + startY = top + selection.y1 - evY(event); + + $box.unbind('mousemove', areaMouseMove); + + $(document).mousemove(movingMouseMove) + .one('mouseup', function () { + options.onSelectEnd(img, getSelection()); + + $(document).unbind('mousemove', movingMouseMove); + $box.mousemove(areaMouseMove); + }); + } + else + $img.mousedown(event); + + return false; + } + + function fixAspectRatio(xFirst) { + if (aspectRatio) + if (xFirst) { + x2 = max(left, min(left + imgWidth, + x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))); + + y2 = round(max(top, min(top + imgHeight, + y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)))); + x2 = round(x2); + } + else { + y2 = max(top, min(top + imgHeight, + y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))); + x2 = round(max(left, min(left + imgWidth, + x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)))); + y2 = round(y2); + } + } + + function doResize() { + x1 = min(x1, left + imgWidth); + y1 = min(y1, top + imgHeight); + + if (abs(x2 - x1) < minWidth) { + x2 = x1 - minWidth * (x2 < x1 || -1); + + if (x2 < left) + x1 = left + minWidth; + else if (x2 > left + imgWidth) + x1 = left + imgWidth - minWidth; + } + + if (abs(y2 - y1) < minHeight) { + y2 = y1 - minHeight * (y2 < y1 || -1); + + if (y2 < top) + y1 = top + minHeight; + else if (y2 > top + imgHeight) + y1 = top + imgHeight - minHeight; + } + + x2 = max(left, min(x2, left + imgWidth)); + y2 = max(top, min(y2, top + imgHeight)); + + fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio); + + if (abs(x2 - x1) > maxWidth) { + x2 = x1 - maxWidth * (x2 < x1 || -1); + fixAspectRatio(); + } + + if (abs(y2 - y1) > maxHeight) { + y2 = y1 - maxHeight * (y2 < y1 || -1); + fixAspectRatio(true); + } + + selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)), + y1: selY(min(y1, y2)), y2: selY(max(y1, y2)), + width: abs(x2 - x1), height: abs(y2 - y1) }; + + update(); + + options.onSelectChange(img, getSelection()); + } + + function selectingMouseMove(event) { + x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2); + y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2); + + doResize(); + + return false; + + } + + function doMove(newX1, newY1) { + x2 = (x1 = newX1) + selection.width; + y2 = (y1 = newY1) + selection.height; + + $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2), + y2: selY(y2) }); + + update(); + + options.onSelectChange(img, getSelection()); + } + + function movingMouseMove(event) { + x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width)); + y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height)); + + doMove(x1, y1); + + event.preventDefault(); + + return false; + } + + function startSelection() { + $(document).unbind('mousemove', startSelection); + adjust(); + + x2 = x1; + y2 = y1; + + doResize(); + + resize = ''; + + if (!$outer.is(':visible')) + $box.add($outer).hide().fadeIn(options.fadeSpeed||0); + + shown = true; + + $(document).unbind('mouseup', cancelSelection) + .mousemove(selectingMouseMove).one('mouseup', docMouseUp); + $box.unbind('mousemove', areaMouseMove); + + options.onSelectStart(img, getSelection()); + } + + function cancelSelection() { + $(document).unbind('mousemove', startSelection) + .unbind('mouseup', cancelSelection); + hide($box.add($outer)); + + setSelection(selX(x1), selY(y1), selX(x1), selY(y1)); + + if (!(this instanceof $.imgAreaSelect)) { + options.onSelectChange(img, getSelection()); + options.onSelectEnd(img, getSelection()); + } + } + + function imgMouseDown(event) { + if (event.which != 1 || $outer.is(':animated')) return false; + + adjust(); + startX = x1 = evX(event); + startY = y1 = evY(event); + + $(document).mousemove(startSelection).mouseup(cancelSelection); + + return false; + } + + function windowResize() { + doUpdate(false); + } + + function imgLoad() { + imgLoaded = true; + + setOptions(options = $.extend({ + classPrefix: 'imgareaselect', + movable: true, + parent: 'body', + resizable: true, + resizeMargin: 10, + onInit: function () {}, + onSelectStart: function () {}, + onSelectChange: function () {}, + onSelectEnd: function () {} + }, options)); + + $box.add($outer).css({ visibility: '' }); + + if (options.show) { + shown = true; + adjust(); + update(); + $box.add($outer).hide().fadeIn(options.fadeSpeed||0); + } + + setTimeout(function () { options.onInit(img, getSelection()); }, 0); + } + + var docKeyPress = function(event) { + var k = options.keys, d, t, key = event.keyCode; + + d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt : + !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl : + !isNaN(k.shift) && event.shiftKey ? k.shift : + !isNaN(k.arrows) ? k.arrows : 10; + + if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) || + (k.ctrl == 'resize' && event.ctrlKey) || + (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey))) + { + switch (key) { + case 37: + d = -d; + case 39: + t = max(x1, x2); + x1 = min(x1, x2); + x2 = max(t + d, x1); + fixAspectRatio(); + break; + case 38: + d = -d; + case 40: + t = max(y1, y2); + y1 = min(y1, y2); + y2 = max(t + d, y1); + fixAspectRatio(true); + break; + default: + return; + } + + doResize(); + } + else { + x1 = min(x1, x2); + y1 = min(y1, y2); + + switch (key) { + case 37: + doMove(max(x1 - d, left), y1); + break; + case 38: + doMove(x1, max(y1 - d, top)); + break; + case 39: + doMove(x1 + min(d, imgWidth - selX(x2)), y1); + break; + case 40: + doMove(x1, y1 + min(d, imgHeight - selY(y2))); + break; + default: + return; + } + } + + return false; + }; + + function styleOptions($elem, props) { + for (var option in props) + if (options[option] !== undefined) + $elem.css(props[option], options[option]); + } + + function setOptions(newOptions) { + if (newOptions.parent) + ($parent = $(newOptions.parent)).append($box.add($outer)); + + $.extend(options, newOptions); + + adjust(); + + if (newOptions.handles != null) { + $handles.remove(); + $handles = $([]); + + i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0; + + while (i--) + $handles = $handles.add(div()); + + $handles.addClass(options.classPrefix + '-handle').css({ + position: 'absolute', + fontSize: 0, + zIndex: zIndex + 1 || 1 + }); + + if (!parseInt($handles.css('width')) >= 0) + $handles.width(5).height(5); + + if (o = options.borderWidth) + $handles.css({ borderWidth: o, borderStyle: 'solid' }); + + styleOptions($handles, { borderColor1: 'border-color', + borderColor2: 'background-color', + borderOpacity: 'opacity' }); + } + + scaleX = options.imageWidth / imgWidth || 1; + scaleY = options.imageHeight / imgHeight || 1; + + if (newOptions.x1 != null) { + setSelection(newOptions.x1, newOptions.y1, newOptions.x2, + newOptions.y2); + newOptions.show = !newOptions.hide; + } + + if (newOptions.keys) + options.keys = $.extend({ shift: 1, ctrl: 'resize' }, + newOptions.keys); + + $outer.addClass(options.classPrefix + '-outer'); + $area.addClass(options.classPrefix + '-selection'); + for (i = 0; i++ < 4;) + $($border[i-1]).addClass(options.classPrefix + '-border' + i); + + styleOptions($area, { selectionColor: 'background-color', + selectionOpacity: 'opacity' }); + styleOptions($border, { borderOpacity: 'opacity', + borderWidth: 'border-width' }); + styleOptions($outer, { outerColor: 'background-color', + outerOpacity: 'opacity' }); + if (o = options.borderColor1) + $($border[0]).css({ borderStyle: 'solid', borderColor: o }); + if (o = options.borderColor2) + $($border[1]).css({ borderStyle: 'dashed', borderColor: o }); + + $box.append($area.add($border).add($areaOpera)).append($handles); + + if (msie) { + if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/)) + $outer.css('opacity', o[1]/100); + if (o = ($border.css('filter')||'').match(/opacity=(\d+)/)) + $border.css('opacity', o[1]/100); + } + + if (newOptions.hide) + hide($box.add($outer)); + else if (newOptions.show && imgLoaded) { + shown = true; + $box.add($outer).fadeIn(options.fadeSpeed||0); + doUpdate(); + } + + aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1]; + + $img.add($outer).unbind('mousedown', imgMouseDown); + + if (options.disable || options.enable === false) { + $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown); + $(window).unbind('resize', windowResize); + } + else { + if (options.enable || options.disable === false) { + if (options.resizable || options.movable) + $box.mousemove(areaMouseMove).mousedown(areaMouseDown); + + $(window).resize(windowResize); + } + + if (!options.persistent) + $img.add($outer).mousedown(imgMouseDown); + } + + options.enable = options.disable = undefined; + } + + this.remove = function () { + setOptions({ disable: true }); + $box.add($outer).remove(); + }; + + this.getOptions = function () { return options; }; + + this.setOptions = setOptions; + + this.getSelection = getSelection; + + this.setSelection = setSelection; + + this.cancelSelection = cancelSelection; + + this.update = doUpdate; + + var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1], + opera = /opera/i.test(ua), + safari = /webkit/i.test(ua) && !/chrome/i.test(ua); + + $p = $img; + + while ($p.length) { + zIndex = max(zIndex, + !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex); + if ($p.css('position') == 'fixed') + position = 'fixed'; + + $p = $p.parent(':not(body)'); + } + + zIndex = options.zIndex || zIndex; + + if (msie) + $img.attr('unselectable', 'on'); + + $.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress'; + + if (opera) + + $areaOpera = div().css({ width: '100%', height: '100%', + position: 'absolute', zIndex: zIndex + 2 || 2 }); + + $box.add($outer).css({ visibility: 'hidden', position: position, + overflow: 'hidden', zIndex: zIndex || '0' }); + $box.css({ zIndex: zIndex + 2 || 2 }); + $area.add($border).css({ position: 'absolute', fontSize: 0 }); + + img.complete || img.readyState == 'complete' || !$img.is('img') ? + imgLoad() : $img.one('load', imgLoad); + + if (!imgLoaded && msie && msie >= 7) + img.src = img.src; +}; + +$.fn.imgAreaSelect = function (options) { + options = options || {}; + + this.each(function () { + if ($(this).data('imgAreaSelect')) { + if (options.remove) { + $(this).data('imgAreaSelect').remove(); + $(this).removeData('imgAreaSelect'); + } + else + $(this).data('imgAreaSelect').setOptions(options); + } + else if (!options.remove) { + if (options.enable === undefined && options.disable === undefined) + options.enable = true; + + $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options)); + } + }); + + if (options.instance) + return $(this).data('imgAreaSelect'); + + return this; +}; + +})(jQuery); diff --git a/static/pygal/js/imgareaselect-js/jquery.imgareaselect.min.js b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.min.js new file mode 100644 index 0000000..04babcc --- /dev/null +++ b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.min.js @@ -0,0 +1 @@ +(function($){var abs=Math.abs,max=Math.max,min=Math.min,round=Math.round;function div(){return $('
')}$.imgAreaSelect=function(img,options){var $img=$(img),imgLoaded,$box=div(),$area=div(),$border=div().add(div()).add(div()).add(div()),$outer=div().add(div()).add(div()).add(div()),$handles=$([]),$areaOpera,left,top,imgOfs={left:0,top:0},imgWidth,imgHeight,$parent,parOfs={left:0,top:0},zIndex=0,position='absolute',startX,startY,scaleX,scaleY,resize,minWidth,minHeight,maxWidth,maxHeight,aspectRatio,shown,x1,y1,x2,y2,selection={x1:0,y1:0,x2:0,y2:0,width:0,height:0},docElem=document.documentElement,ua=navigator.userAgent,$p,d,i,o,w,h,adjusted;function viewX(x){return x+imgOfs.left-parOfs.left}function viewY(y){return y+imgOfs.top-parOfs.top}function selX(x){return x-imgOfs.left+parOfs.left}function selY(y){return y-imgOfs.top+parOfs.top}function evX(event){return event.pageX-parOfs.left}function evY(event){return event.pageY-parOfs.top}function getSelection(noScale){var sx=noScale||scaleX,sy=noScale||scaleY;return{x1:round(selection.x1*sx),y1:round(selection.y1*sy),x2:round(selection.x2*sx),y2:round(selection.y2*sy),width:round(selection.x2*sx)-round(selection.x1*sx),height:round(selection.y2*sy)-round(selection.y1*sy)}}function setSelection(x1,y1,x2,y2,noScale){var sx=noScale||scaleX,sy=noScale||scaleY;selection={x1:round(x1/sx||0),y1:round(y1/sy||0),x2:round(x2/sx||0),y2:round(y2/sy||0)};selection.width=selection.x2-selection.x1;selection.height=selection.y2-selection.y1}function adjust(){if(!imgLoaded||!$img.width())return;imgOfs={left:round($img.offset().left),top:round($img.offset().top)};imgWidth=$img.innerWidth();imgHeight=$img.innerHeight();imgOfs.top+=($img.outerHeight()-imgHeight)>>1;imgOfs.left+=($img.outerWidth()-imgWidth)>>1;minWidth=round(options.minWidth/scaleX)||0;minHeight=round(options.minHeight/scaleY)||0;maxWidth=round(min(options.maxWidth/scaleX||1<<24,imgWidth));maxHeight=round(min(options.maxHeight/scaleY||1<<24,imgHeight));if($().jquery=='1.3.2'&&position=='fixed'&&!docElem['getBoundingClientRect']){imgOfs.top+=max(document.body.scrollTop,docElem.scrollTop);imgOfs.left+=max(document.body.scrollLeft,docElem.scrollLeft)}parOfs=/absolute|relative/.test($parent.css('position'))?{left:round($parent.offset().left)-$parent.scrollLeft(),top:round($parent.offset().top)-$parent.scrollTop()}:position=='fixed'?{left:$(document).scrollLeft(),top:$(document).scrollTop()}:{left:0,top:0};left=viewX(0);top=viewY(0);if(selection.x2>imgWidth||selection.y2>imgHeight)doResize()}function update(resetKeyPress){if(!shown)return;$box.css({left:viewX(selection.x1),top:viewY(selection.y1)}).add($area).width(w=selection.width).height(h=selection.height);$area.add($border).add($handles).css({left:0,top:0});$border.width(max(w-$border.outerWidth()+$border.innerWidth(),0)).height(max(h-$border.outerHeight()+$border.innerHeight(),0));$($outer[0]).css({left:left,top:top,width:selection.x1,height:imgHeight});$($outer[1]).css({left:left+selection.x1,top:top,width:w,height:selection.y1});$($outer[2]).css({left:left+selection.x2,top:top,width:imgWidth-selection.x2,height:imgHeight});$($outer[3]).css({left:left+selection.x1,top:top+selection.y2,width:w,height:imgHeight-selection.y2});w-=$handles.outerWidth();h-=$handles.outerHeight();switch($handles.length){case 8:$($handles[4]).css({left:w>>1});$($handles[5]).css({left:w,top:h>>1});$($handles[6]).css({left:w>>1,top:h});$($handles[7]).css({top:h>>1});case 4:$handles.slice(1,3).css({left:w});$handles.slice(2,4).css({top:h})}if(resetKeyPress!==false){if($.imgAreaSelect.onKeyPress!=docKeyPress)$(document).unbind($.imgAreaSelect.keyPress,$.imgAreaSelect.onKeyPress);if(options.keys)$(document)[$.imgAreaSelect.keyPress]($.imgAreaSelect.onKeyPress=docKeyPress)}if(msie&&$border.outerWidth()-$border.innerWidth()==2){$border.css('margin',0);setTimeout(function(){$border.css('margin','auto')},0)}}function doUpdate(resetKeyPress){adjust();update(resetKeyPress);x1=viewX(selection.x1);y1=viewY(selection.y1);x2=viewX(selection.x2);y2=viewY(selection.y2)}function hide($elem,fn){options.fadeSpeed?$elem.fadeOut(options.fadeSpeed,fn):$elem.hide()}function areaMouseMove(event){var x=selX(evX(event))-selection.x1,y=selY(evY(event))-selection.y1;if(!adjusted){adjust();adjusted=true;$box.one('mouseout',function(){adjusted=false})}resize='';if(options.resizable){if(y<=options.resizeMargin)resize='n';else if(y>=selection.height-options.resizeMargin)resize='s';if(x<=options.resizeMargin)resize+='w';else if(x>=selection.width-options.resizeMargin)resize+='e'}$box.css('cursor',resize?resize+'-resize':options.movable?'move':'');if($areaOpera)$areaOpera.toggle()}function docMouseUp(event){$('body').css('cursor','');if(options.autoHide||selection.width*selection.height==0)hide($box.add($outer),function(){$(this).hide()});$(document).unbind('mousemove',selectingMouseMove);$box.mousemove(areaMouseMove);options.onSelectEnd(img,getSelection())}function areaMouseDown(event){if(event.which!=1)return false;adjust();if(resize){$('body').css('cursor',resize+'-resize');x1=viewX(selection[/w/.test(resize)?'x2':'x1']);y1=viewY(selection[/n/.test(resize)?'y2':'y1']);$(document).mousemove(selectingMouseMove).one('mouseup',docMouseUp);$box.unbind('mousemove',areaMouseMove)}else if(options.movable){startX=left+selection.x1-evX(event);startY=top+selection.y1-evY(event);$box.unbind('mousemove',areaMouseMove);$(document).mousemove(movingMouseMove).one('mouseup',function(){options.onSelectEnd(img,getSelection());$(document).unbind('mousemove',movingMouseMove);$box.mousemove(areaMouseMove)})}else $img.mousedown(event);return false}function fixAspectRatio(xFirst){if(aspectRatio)if(xFirst){x2=max(left,min(left+imgWidth,x1+abs(y2-y1)*aspectRatio*(x2>x1||-1)));y2=round(max(top,min(top+imgHeight,y1+abs(x2-x1)/aspectRatio*(y2>y1||-1))));x2=round(x2)}else{y2=max(top,min(top+imgHeight,y1+abs(x2-x1)/aspectRatio*(y2>y1||-1)));x2=round(max(left,min(left+imgWidth,x1+abs(y2-y1)*aspectRatio*(x2>x1||-1))));y2=round(y2)}}function doResize(){x1=min(x1,left+imgWidth);y1=min(y1,top+imgHeight);if(abs(x2-x1)left+imgWidth)x1=left+imgWidth-minWidth}if(abs(y2-y1)top+imgHeight)y1=top+imgHeight-minHeight}x2=max(left,min(x2,left+imgWidth));y2=max(top,min(y2,top+imgHeight));fixAspectRatio(abs(x2-x1)maxWidth){x2=x1-maxWidth*(x2maxHeight){y2=y1-maxHeight*(y2=0)$handles.width(5).height(5);if(o=options.borderWidth)$handles.css({borderWidth:o,borderStyle:'solid'});styleOptions($handles,{borderColor1:'border-color',borderColor2:'background-color',borderOpacity:'opacity'})}scaleX=options.imageWidth/imgWidth||1;scaleY=options.imageHeight/imgHeight||1;if(newOptions.x1!=null){setSelection(newOptions.x1,newOptions.y1,newOptions.x2,newOptions.y2);newOptions.show=!newOptions.hide}if(newOptions.keys)options.keys=$.extend({shift:1,ctrl:'resize'},newOptions.keys);$outer.addClass(options.classPrefix+'-outer');$area.addClass(options.classPrefix+'-selection');for(i=0;i++<4;)$($border[i-1]).addClass(options.classPrefix+'-border'+i);styleOptions($area,{selectionColor:'background-color',selectionOpacity:'opacity'});styleOptions($border,{borderOpacity:'opacity',borderWidth:'border-width'});styleOptions($outer,{outerColor:'background-color',outerOpacity:'opacity'});if(o=options.borderColor1)$($border[0]).css({borderStyle:'solid',borderColor:o});if(o=options.borderColor2)$($border[1]).css({borderStyle:'dashed',borderColor:o});$box.append($area.add($border).add($areaOpera)).append($handles);if(msie){if(o=($outer.css('filter')||'').match(/opacity=(\d+)/))$outer.css('opacity',o[1]/100);if(o=($border.css('filter')||'').match(/opacity=(\d+)/))$border.css('opacity',o[1]/100)}if(newOptions.hide)hide($box.add($outer));else if(newOptions.show&&imgLoaded){shown=true;$box.add($outer).fadeIn(options.fadeSpeed||0);doUpdate()}aspectRatio=(d=(options.aspectRatio||'').split(/:/))[0]/d[1];$img.add($outer).unbind('mousedown',imgMouseDown);if(options.disable||options.enable===false){$box.unbind('mousemove',areaMouseMove).unbind('mousedown',areaMouseDown);$(window).unbind('resize',windowResize)}else{if(options.enable||options.disable===false){if(options.resizable||options.movable)$box.mousemove(areaMouseMove).mousedown(areaMouseDown);$(window).resize(windowResize)}if(!options.persistent)$img.add($outer).mousedown(imgMouseDown)}options.enable=options.disable=undefined}this.remove=function(){setOptions({disable:true});$box.add($outer).remove()};this.getOptions=function(){return options};this.setOptions=setOptions;this.getSelection=getSelection;this.setSelection=setSelection;this.cancelSelection=cancelSelection;this.update=doUpdate;var msie=(/msie ([\w.]+)/i.exec(ua)||[])[1],opera=/opera/i.test(ua),safari=/webkit/i.test(ua)&&!/chrome/i.test(ua);$p=$img;while($p.length){zIndex=max(zIndex,!isNaN($p.css('z-index'))?$p.css('z-index'):zIndex);if($p.css('position')=='fixed')position='fixed';$p=$p.parent(':not(body)')}zIndex=options.zIndex||zIndex;if(msie)$img.attr('unselectable','on');$.imgAreaSelect.keyPress=msie||safari?'keydown':'keypress';if(opera)$areaOpera=div().css({width:'100%',height:'100%',position:'absolute',zIndex:zIndex+2||2});$box.add($outer).css({visibility:'hidden',position:position,overflow:'hidden',zIndex:zIndex||'0'});$box.css({zIndex:zIndex+2||2});$area.add($border).css({position:'absolute',fontSize:0});img.complete||img.readyState=='complete'||!$img.is('img')?imgLoad():$img.one('load',imgLoad);if(!imgLoaded&&msie&&msie>=7)img.src=img.src};$.fn.imgAreaSelect=function(options){options=options||{};this.each(function(){if($(this).data('imgAreaSelect')){if(options.remove){$(this).data('imgAreaSelect').remove();$(this).removeData('imgAreaSelect')}else $(this).data('imgAreaSelect').setOptions(options)}else if(!options.remove){if(options.enable===undefined&&options.disable===undefined)options.enable=true;$(this).data('imgAreaSelect',new $.imgAreaSelect(this,options))}});if(options.instance)return $(this).data('imgAreaSelect');return this}})(jQuery); \ No newline at end of file diff --git a/static/pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js new file mode 100644 index 0000000..bde7a21 --- /dev/null +++ b/static/pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js @@ -0,0 +1 @@ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(m($){18 W=2v.4T,D=2v.4S,F=2v.4R,u=2v.4Q;m V(){C $("<4P/>")};$.N=m(T,c){18 O=$(T),1F,A=V(),1k=V(),I=V().r(V()).r(V()).r(V()),B=V().r(V()).r(V()).r(V()),E=$([]),1K,G,l,17={v:0,l:0},Q,M,1l,1g={v:0,l:0},12=0,1J="1H",2k,2j,1t,1s,S,1B,1A,2o,2n,14,1Q,a,b,j,g,f={a:0,b:0,j:0,g:0,H:0,L:0},2u=R.4O,1M=4N.4M,$p,d,i,o,w,h,2p;m 1n(x){C x+17.v-1g.v};m 1m(y){C y+17.l-1g.l};m 1b(x){C x-17.v+1g.v};m 1a(y){C y-17.l+1g.l};m 1z(3J){C 3J.4L-1g.v};m 1y(3I){C 3I.4K-1g.l};m 13(32){18 1i=32||1t,1h=32||1s;C{a:u(f.a*1i),b:u(f.b*1h),j:u(f.j*1i),g:u(f.g*1h),H:u(f.j*1i)-u(f.a*1i),L:u(f.g*1h)-u(f.b*1h)}};m 23(a,b,j,g,31){18 1i=31||1t,1h=31||1s;f={a:u(a/1i||0),b:u(b/1h||0),j:u(j/1i||0),g:u(g/1h||0)};f.H=f.j-f.a;f.L=f.g-f.b};m 1f(){9(!1F||!O.H()){C}17={v:u(O.2t().v),l:u(O.2t().l)};Q=O.2Y();M=O.3H();17.l+=(O.30()-M)>>1;17.v+=(O.2q()-Q)>>1;1B=u(c.4J/1t)||0;1A=u(c.4I/1s)||0;2o=u(F(c.4H/1t||1<<24,Q));2n=u(F(c.4G/1s||1<<24,M));9($().4F=="1.3.2"&&1J=="21"&&!2u["4E"]){17.l+=D(R.1q.2r,2u.2r);17.v+=D(R.1q.2s,2u.2s)}1g=/1H|4D/.1c(1l.q("1p"))?{v:u(1l.2t().v)-1l.2s(),l:u(1l.2t().l)-1l.2r()}:1J=="21"?{v:$(R).2s(),l:$(R).2r()}:{v:0,l:0};G=1n(0);l=1m(0);9(f.j>Q||f.g>M){1U()}};m 1V(3F){9(!1Q){C}A.q({v:1n(f.a),l:1m(f.b)}).r(1k).H(w=f.H).L(h=f.L);1k.r(I).r(E).q({v:0,l:0});I.H(D(w-I.2q()+I.2Y(),0)).L(D(h-I.30()+I.3H(),0));$(B[0]).q({v:G,l:l,H:f.a,L:M});$(B[1]).q({v:G+f.a,l:l,H:w,L:f.b});$(B[2]).q({v:G+f.j,l:l,H:Q-f.j,L:M});$(B[3]).q({v:G+f.a,l:l+f.g,H:w,L:M-f.g});w-=E.2q();h-=E.30();2O(E.3f){15 8:$(E[4]).q({v:w>>1});$(E[5]).q({v:w,l:h>>1});$(E[6]).q({v:w>>1,l:h});$(E[7]).q({l:h>>1});15 4:E.3G(1,3).q({v:w});E.3G(2,4).q({l:h})}9(3F!==Y){9($.N.2Z!=2R){$(R).U($.N.2z,$.N.2Z)}9(c.1T){$(R)[$.N.2z]($.N.2Z=2R)}}9(1j&&I.2q()-I.2Y()==2){I.q("3E",0);3x(m(){I.q("3E","4C")},0)}};m 22(3D){1f();1V(3D);a=1n(f.a);b=1m(f.b);j=1n(f.j);g=1m(f.g)};m 27(2X,2w){c.1P?2X.4B(c.1P,2w):2X.1r()};m 1d(2W){18 x=1b(1z(2W))-f.a,y=1a(1y(2W))-f.b;9(!2p){1f();2p=11;A.1G("4A",m(){2p=Y})}S="";9(c.2D){9(y<=c.1W){S="n"}X{9(y>=f.L-c.1W){S="s"}}9(x<=c.1W){S+="w"}X{9(x>=f.H-c.1W){S+="e"}}}A.q("2V",S?S+"-19":c.26?"4z":"");9(1K){1K.4y()}};m 2S(4x){$("1q").q("2V","");9(c.4w||f.H*f.L==0){27(A.r(B),m(){$(J).1r()})}$(R).U("P",2l);A.P(1d);c.2f(T,13())};m 2C(1X){9(1X.3z!=1){C Y}1f();9(S){$("1q").q("2V",S+"-19");a=1n(f[/w/.1c(S)?"j":"a"]);b=1m(f[/n/.1c(S)?"g":"b"]);$(R).P(2l).1G("1x",2S);A.U("P",1d)}X{9(c.26){2k=G+f.a-1z(1X);2j=l+f.b-1y(1X);A.U("P",1d);$(R).P(2T).1G("1x",m(){c.2f(T,13());$(R).U("P",2T);A.P(1d)})}X{O.1O(1X)}}C Y};m 1w(3C){9(14){9(3C){j=D(G,F(G+Q,a+W(g-b)*14*(j>a||-1)));g=u(D(l,F(l+M,b+W(j-a)/14*(g>b||-1))));j=u(j)}X{g=D(l,F(l+M,b+W(j-a)/14*(g>b||-1)));j=u(D(G,F(G+Q,a+W(g-b)*14*(j>a||-1))));g=u(g)}}};m 1U(){a=F(a,G+Q);b=F(b,l+M);9(W(j-a)<1B){j=a-1B*(jG+Q){a=G+Q-1B}}}9(W(g-b)<1A){g=b-1A*(gl+M){b=l+M-1A}}}j=D(G,F(j,G+Q));g=D(l,F(g,l+M));1w(W(j-a)2o){j=a-2o*(j2n){g=b-2n*(g=0){E.H(5).L(5)}9(o=c.2K){E.q({2K:o,2H:"3m"})}1R(E,{3n:"2J-28",3l:"2I-28",3o:"1e"})}1t=c.4l/Q||1;1s=c.4k/M||1;9(K.a!=3q){23(K.a,K.b,K.j,K.g);K.2F=!K.1r}9(K.1T){c.1T=$.2c({2b:1,2a:"19"},K.1T)}B.29(c.1S+"-4j");1k.29(c.1S+"-4i");3p(i=0;i++<4;){$(I[i-1]).29(c.1S+"-2J"+i)}1R(1k,{4h:"2I-28",4g:"1e"});1R(I,{3o:"1e",2K:"2J-H"});1R(B,{4f:"2I-28",4e:"1e"});9(o=c.3n){$(I[0]).q({2H:"3m",3k:o})}9(o=c.3l){$(I[1]).q({2H:"4d",3k:o})}A.2G(1k.r(I).r(1K)).2G(E);9(1j){9(o=(B.q("3j")||"").3i(/1e=(\\d+)/)){B.q("1e",o[1]/1Z)}9(o=(I.q("3j")||"").3i(/1e=(\\d+)/)){I.q("1e",o[1]/1Z)}}9(K.1r){27(A.r(B))}X{9(K.2F&&1F){1Q=11;A.r(B).2E(c.1P||0);22()}}14=(d=(c.4c||"").4b(/:/))[0]/d[1];O.r(B).U("1O",2A);9(c.1E||c.1D===Y){A.U("P",1d).U("1O",2C);$(3h).U("19",2B)}X{9(c.1D||c.1E===Y){9(c.2D||c.26){A.P(1d).1O(2C)}$(3h).19(2B)}9(!c.4a){O.r(B).1O(2A)}}c.1D=c.1E=1Y};J.1o=m(){25({1E:11});A.r(B).1o()};J.49=m(){C c};J.33=25;J.48=13;J.47=23;J.46=1N;J.45=22;18 1j=(/44 ([\\w.]+)/i.43(1M)||[])[1],3c=/42/i.1c(1M),3d=/41/i.1c(1M)&&!/3Z/i.1c(1M);$p=O;3g($p.3f){12=D(12,!1L($p.q("z-3e"))?$p.q("z-3e"):12);9($p.q("1p")=="21"){1J="21"}$p=$p.20(":3Y(1q)")}12=c.1I||12;9(1j){O.3X("3W","3V")}$.N.2z=1j||3d?"3U":"3T";9(3c){1K=V().q({H:"1Z%",L:"1Z%",1p:"1H",1I:12+2||2})}A.r(B).q({3b:"3a",1p:1J,3S:"3a",1I:12||"0"});A.q({1I:12+2||2});1k.r(I).q({1p:"1H",36:0});T.35||T.3R=="35"||!O.2y("3Q")?2x():O.1G("3P",2x);9(!1F&&1j&&1j>=7){T.34=T.34}};$.2w.N=m(Z){Z=Z||{};J.3O(m(){9($(J).1C("N")){9(Z.1o){$(J).1C("N").1o();$(J).3N("N")}X{$(J).1C("N").33(Z)}}X{9(!Z.1o){9(Z.1D===1Y&&Z.1E===1Y){Z.1D=11}$(J).1C("N",3M $.N(J,Z))}}});9(Z.3L){C $(J).1C("N")}C J}})(3K);',62,304,'|||||||||if|x1|y1|_7|||_23|y2|||x2||top|function||||css|add|||_4|left|||||_a|_d|return|_2|_e|_3|_10|width|_c|this|_55|height|_13|imgAreaSelect|_8|mousemove|_12|document|_1c|_6|unbind|_5|_1|else|false|_58||true|_16|_2c|_21|case|_50|_11|var|resize|_29|_28|test|_3a|opacity|_30|_15|sy|sx|_35|_b|_14|_27|_26|remove|position|body|hide|_1b|_1a|break|_45|_42|mouseup|evY|evX|_1e|_1d|data|enable|disable|_9|one|absolute|zIndex|_17|_f|isNaN|ua|_4a|mousedown|fadeSpeed|_22|_51|classPrefix|keys|_31|_32|resizeMargin|_40|undefined|100|parent|fixed|_36|_2e||_4f|movable|_38|color|addClass|ctrl|shift|extend|_54|altKey|onSelectEnd|onSelectChange|_49|_4c|_19|_18|_3e|_48|_20|_1f|_25|outerWidth|scrollTop|scrollLeft|offset|_24|Math|fn|_4e|is|keyPress|_4b|_4d|_3f|resizable|fadeIn|show|append|borderStyle|background|border|borderWidth|handles|_53|key|switch|alt|arrows|_34|_3c|_41|_44|cursor|_3b|_39|innerWidth|onKeyPress|outerHeight|_2f|_2d|setOptions|src|complete|fontSize||||hidden|visibility|_56|_57|index|length|while|window|match|filter|borderColor|borderColor2|solid|borderColor1|borderOpacity|for|null|_52|default|originalEvent|ctrlKey|shiftKey|onInit|setTimeout|onSelectStart|which|_47|_46|_43|_37|margin|_33|slice|innerHeight|_2b|_2a|jQuery|instance|new|removeData|each|load|img|readyState|overflow|keypress|keydown|on|unselectable|attr|not|chrome||webkit|opera|exec|msie|update|cancelSelection|setSelection|getSelection|getOptions|persistent|split|aspectRatio|dashed|outerOpacity|outerColor|selectionOpacity|selectionColor|selection|outer|imageHeight|imageWidth|parseInt|handle|corners|in|keyCode|imgareaselect|animated|instanceof|visible|preventDefault|autoHide|_3d|toggle|move|mouseout|fadeOut|auto|relative|getBoundingClientRect|jquery|maxHeight|maxWidth|minHeight|minWidth|pageY|pageX|userAgent|navigator|documentElement|div|round|min|max|abs'.split('|'))) diff --git a/static/pygal/js/jquery.min.js b/static/pygal/js/jquery.min.js new file mode 100644 index 0000000..50d1b22 --- /dev/null +++ b/static/pygal/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("