diff --git a/pandora/app/config.py b/pandora/app/config.py index 78eb5d80..b9275468 100644 --- a/pandora/app/config.py +++ b/pandora/app/config.py @@ -94,10 +94,20 @@ def load_config(init=False): for key in config['itemKeys']: config['keys'][key['id']] = key + # add entities if needed + if len(config.get('entities', [])) and not [k for k in config['documentKeys'] if k['id'] == 'entites']: + config['documentKeys'].append({ + 'id': 'entity', + 'title': 'Entity', + 'type': 'string', + 'find': True + }) # add missing defaults for section in sorted(( - 'capabilities', 'cantPlay', 'entities', 'itemName', 'itemTitleKeys', 'media', 'posters', + 'capabilities', 'cantPlay', + 'documentKeys', + 'entities', 'itemName', 'itemTitleKeys', 'itemKeys', 'media', 'posters', 'site', 'tv', 'user.ui', 'user.ui.part', 'user.ui.showFolder', 'menuExtras', 'languages' )): diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 618d5f27..3de86536 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -35,11 +35,13 @@ */ "capabilities": { "canAddItems": {"staff": true, "admin": true}, + "canAddDocuments": {"staff": true, "admin": true}, "canDownloadVideo": {"guest": -1, "member": -1, "friend": -1, "staff": -1, "admin": -1}, "canEditAnnotations": {"staff": true, "admin": true}, "canEditEntities": {"staff": true, "admin": true}, "canEditDocuments": {"staff": true, "admin": true}, "canEditEvents": {"staff": true, "admin": true}, + "canEditFeaturedCollections": {"staff": true, "admin": true}, "canEditFeaturedEdits": {"staff": true, "admin": true}, "canEditFeaturedLists": {"staff": true, "admin": true}, "canEditFeaturedTexts": {"staff": true, "admin": true}, @@ -61,11 +63,13 @@ "canPlayVideo": {"guest": 1, "member": 1, "friend": 4, "staff": 4, "admin": 4}, "canReadText": {"guest": 0, "member": 0, "friend": 1, "staff": 1, "admin": 1}, "canRemoveItems": {"admin": true}, + "canRemoveDocuments": {"staff": true, "admin": true}, "canSeeAccessed": {"staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"staff": true, "admin": true}, "canSeeExtraItemViews": {"staff": true, "admin": true}, "canSeeMedia": {"staff": true, "admin": true}, + "canSeeDocument": {"guest": 1, "member": 1, "firend": 4, "staff": 4, "admin": 4}, "canSeeItem": {"guest": 3, "member": 3, "friend": 4, "staff": 4, "admin": 4}, "canSeeSize": {"friend": true, "staff": true, "admin": true}, "canSeeSoftwareVersion": {"staff": true, "admin": true}, @@ -91,6 +95,212 @@ list means it will not be included in find annotations. */ "clipLayers": ["subtitles"], + "documentKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "operator": "+", + "title": "Title", + "type": "string", + "find": true, + "sort": true, + "sortType": "title", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "type", + "operator": "+", + "title": "Type", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "author", + "operator": "+", + "title": "Author", + "type": ["string"], + "filter": true, + "find": true, + "sort": true, + "sortType": "person", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "publisher", + "operator": "+", + "title": "Publisher", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "place", + "title": "Place", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "date", + "title": "Date", + "type": "string", + "columnWidth": 120, + //"format": {"type": "date", "args": ["%a, %b %e, %Y"]}, + "sort": true + }, + { + "id": "series", + "title": "Series", + "type": "string", + "columnWidth": 128, + "find": true, + "sort": true + }, + { + "id": "edition", + "title": "Edition", + "type": "string", + "columnWidth": 128, + "find": true + }, + { + "id": "language", + "title": "Language", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "id", + "operator": "+", + "title": "ID", + "type": "string", + "sort": true, + "columnWidth": 64 + }, + { + "id": "extension", + "operator": "+", + "title": "Extension", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 64 + }, + { + "id": "dimensions", + "operator": "-", + "title": "Dimensions", + "type": "integer", + "sort": true, + "columnWidth": 128 + }, + { + "id": "size", + "operator": "-", + "title": "Size", + "type": "integer", + "sort": true, + "format": {"type": "value", "args": ["B"]}, + "columnWidth": 64 + }, + { + "id": "description", + "operator": "+", + "title": "Description", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "matches", + "operator": "-", + "title": "Matches", + "type": "integer", + "sort": true, + "columnWidth": 64 + }, + { + "id": "user", + "operator": "+", + "title": "User", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "created", + "operator": "-", + "title": "Created", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "modified", + "operator": "-", + "title": "Modified", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 150, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Out of Copyright", "Under Copyright", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + } + ], /* "entities" can be used to store arbitrary data. They can be referenced in annotations, info view, or elsewhere. Each entry defines a specific class @@ -976,13 +1186,24 @@ "calendarFind": "", "calendarSelection": "", "clipColumns": 2, + "collectionColumns": ["title", "id", "extension", "dimensions", "size", "description", "matches", "user", "created", "modified"], + "collectionColumnWidth": {}, + "collectionSelection": [], + "collectionSort": [ + {"key": "title", "operator": "+"}, + {"key": "extension", "operator": "+"} + ], + "collectionView": "grid", + "collections": {}, "columns": { "Colors": { "columns": ["title", "director", "country", "year", "hue", "saturation", "brightness"], "columnWidth": {} } }, + "document": "", "documents": {}, + "documentView": "view", "documentSize": 256, "documentsSelection": {}, "documentsSort": [{"key": "name", "operator": "+"}], @@ -1011,6 +1232,7 @@ ], "filtersSize": 176, "find": {"conditions": [], "operator": "&"}, + "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", "icons": "posters", @@ -1076,7 +1298,7 @@ "featured": true, "volumes": true }, - "texts": { + "documents": { "personal": true, "favorite": true, "featured": true diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index a835e43f..a8b896d4 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -36,11 +36,13 @@ */ "capabilities": { "canAddItems": {"researcher": true, "staff": true, "admin": true}, + "canAddDocuments": {"researcher": true, "staff": true, "admin": true}, "canDownloadVideo": {"guest": -1, "member": -1, "researcher": 3, "staff": 3, "admin": 3}, "canEditAnnotations": {"staff": true, "admin": true}, - "canEditEntities": {"staff": true, "admin": true}, "canEditDocuments": {"researcher": true, "staff": true, "admin": true}, + "canEditEntities": {"staff": true, "admin": true}, "canEditEvents": {"researcher": true, "staff": true, "admin": true}, + "canEditFeaturedCollections": {"staff": true, "admin": true}, "canEditFeaturedEdits": {"staff": true, "admin": true}, "canEditFeaturedLists": {"staff": true, "admin": true}, "canEditFeaturedTexts": {"staff": true, "admin": true}, @@ -63,11 +65,13 @@ "canPlayVideo": {"guest": 1, "member": 1, "researcher": 3, "staff": 3, "admin": 3}, "canReadText": {"guest": 0, "member": 0, "researcher": 1, "staff": 1, "admin": 1}, "canRemoveItems": {"staff": true, "admin": true}, + "canRemoveDocuments": {"staff": true, "admin": true}, "canSeeAccessed": {"researcher": true, "staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"researcher": true, "staff": true, "admin": true}, "canSeeExtraItemViews": {"researcher": true, "staff": true, "admin": true}, "canSeeMedia": {"researcher": true, "staff": true, "admin": true}, + "canSeeDocument": {"guest": 3, "member": 3, "researcher": 3, "staff": 3, "admin": 3}, "canSeeItem": {"guest": 3, "member": 3, "researcher": 3, "staff": 3, "admin": 3}, "canSeeSize": {"researcher": true, "staff": true, "admin": true}, "canSeeSoftwareVersion": {"researcher": true, "staff": true, "admin": true}, @@ -94,6 +98,257 @@ */ "clipLayers": ["subtitles", "keywords", "notes"], /* + "documentKeys" defines the metadata associated with each document. Required keys + are "*", "id" and "title". + A documentKey must have the following properties: + "id": The unique id of the key (as used by the server) + "title": The title of the key (as displayed by the client) + "type": Can be "boolean", "date", "enum", "float", "hue", "integer", + "layer", "string", "text", "time" or ["..."] (list of values of + this type). If type is "layer", this is a reference to the + annotations layer with the same id. + and can have any of the following properties: + "additionalSort": Ordered list of {key, operator} objects, where key is + another itemKey and operator is "+" or "-". This can be used to + override user.ui.listSort when results are sorted by this key. + "autocomplete": If true, the find element will provide autocomplete + "autocompleteSort": Sort order of autocomplete suggestions + "capability": A capability required to see data for this key + "columnRequired": If true, the column can't be removed from list view + "columnWidth": Default column width in px. If absent, no column for + this key can be added in list view. + "filter": If true, one can filter results by this key + "find": If true, this key will appear as an option in the find element + "flag": Can be "country" or "language". If set (and filter is true), a + flag icon corresponding to the field's value will be displayed. + "format": {type: string, args: [value, value, ...]}, used for special + formatting. This will invoke Ox.formatType(args). For details, see + https://oxjs.org/#doc/Ox.formatArea etc. + "secondaryId": If true, loading the URL "/value" will redirect to the + corresponding item, in case there is an exact match for this key + "sort": If true, one can sort results by this key + "sortOperator": Sort order ("+" or "-"), in case it differs from the + default for the key's type ("+" for strings, "-" for numbers) + "sortType": Special sort type ("person" or "title") which can be + further configured in "Manage Names" or "Manage Titles" + "value": {key: string, type: string} or {layer: string, type: string}, + for keys whose value is derived from other keys or layers (like + "number of actors" or "words per minute"). Possible values for type + are "length", "lengthperminute", "words", and "wordsperminute". + Alternatively, "value" can be set to the string "capability", which + results in an itemKey whose boolean value indicates the presence or + absence of a userLevel-dependent capability. This can be used to + create queries and lists like "all items this user can play" etc. + "values": [value, value, ...] Ordered list of values, in case "type" is + "enum" + */ + "documentKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "operator": "+", + "title": "Title", + "type": "string", + "find": true, + "sort": true, + "sortType": "title", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "type", + "operator": "+", + "title": "Type", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "author", + "operator": "+", + "title": "Author", + "type": ["string"], + "filter": true, + "find": true, + "sort": true, + "sortType": "person", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "publisher", + "operator": "+", + "title": "Publisher", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "place", + "title": "Place", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "date", + "title": "Date", + "type": "string", + "columnWidth": 120, + //"format": {"type": "date", "args": ["%a, %b %e, %Y"]}, + "sort": true + }, + { + "id": "series", + "title": "Series", + "type": "string", + "columnWidth": 128, + "find": true, + "sort": true + }, + { + "id": "edition", + "title": "Edition", + "type": "string", + "columnWidth": 128, + "find": true + }, + { + "id": "language", + "title": "Language", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "id", + "operator": "+", + "title": "ID", + "type": "string", + "sort": true, + "columnWidth": 64 + }, + { + "id": "extension", + "operator": "+", + "title": "Extension", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 64 + }, + { + "id": "dimensions", + "operator": "-", + "title": "Dimensions", + "type": "integer", + "sort": true, + "columnWidth": 128 + }, + { + "id": "size", + "operator": "-", + "title": "Size", + "type": "integer", + "sort": true, + "format": {"type": "value", "args": ["B"]}, + "columnWidth": 64 + }, + { + "id": "description", + "operator": "+", + "title": "Description", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "matches", + "operator": "-", + "title": "Matches", + "type": "integer", + "sort": true, + "columnWidth": 64 + }, + { + "id": "user", + "operator": "+", + "title": "User", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "created", + "operator": "-", + "title": "Created", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "modified", + "operator": "-", + "title": "Modified", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 150, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Out of Copyright", "Under Copyright", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + } + ], + /* "entities" can be used to store arbitrary data. They can be referenced in annotations, info view, or elsewhere. Each entry defines a specific class of entity object, its properties and their types (for example an "actor" @@ -1004,16 +1259,26 @@ "calendarFind": "", "calendarSelection": "", "clipColumns": 2, + "collectionColumns": ["title", "id", "extension", "dimensions", "size", "description", "matches", "user", "created", "modified"], + "collectionColumnWidth": {}, + "collectionSelection": [], + "collectionSort": [ + {"key": "title", "operator": "+"}, + {"key": "extension", "operator": "+"} + ], + "collectionView": "grid", + "collections": {}, "columns": { "Colors": { "columns": ["title", "director", "country", "year", "hue", "saturation", "brightness"], "columnWidth": {} } }, + "documentView": "view", "documents": {}, "documentSize": 256, "documentsSelection": {}, - "documentsSort": [{"key": "name", "operator": "+"}], + "documentsSort": [{"key": "title", "operator": "+"}], "documentsView": "grid", "edit": "", "edits": {}, @@ -1039,6 +1304,7 @@ ], "filtersSize": 176, "find": {"conditions": [], "operator": "&"}, + "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", "icons": "posters", @@ -1063,7 +1329,6 @@ "page": "", "part": { "api": "", - "documents": "", "entities": "", "faq": "", "help": "", @@ -1104,6 +1369,11 @@ "featured": true, "volumes": true }, + "documents": { + "personal": true, + "favorite": true, + "featured": true + }, "texts": { "personal": true, "favorite": true, @@ -1117,6 +1387,8 @@ "sidebarSize": 256, "text": "", "texts": {}, + "documents": {}, + "document": "", "theme": "oxmedium", "updateAdvancedFindResults": false, "videoLoop": false, diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index a14f73f0..f68942a1 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -35,11 +35,13 @@ */ "capabilities": { "canAddItems": {"member": true, "staff": true, "admin": true}, + "canAddDocuments": {"member": true, "staff": true, "admin": true}, "canDownloadVideo": {"guest": 0, "member": 0, "staff": 4, "admin": 4}, "canEditAnnotations": {"staff": true, "admin": true}, "canEditEntities": {"staff": true, "admin": true}, "canEditDocuments": {"staff": true, "admin": true}, "canEditEvents": {"staff": true, "admin": true}, + "canEditFeaturedCollections": {"staff": true, "admin": true}, "canEditFeaturedEdits": {"staff": true, "admin": true}, "canEditFeaturedLists": {"staff": true, "admin": true}, "canEditFeaturedTexts": {"staff": true, "admin": true}, @@ -61,11 +63,13 @@ "canPlayVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canReadText": {"guest": 0, "member": 0, "staff": 1, "admin": 1}, "canRemoveItems": {"admin": true}, + "canRemoveDocuments": {"staff": true, "admin": true}, "canSeeAccessed": {"staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"staff": true, "admin": true}, "canSeeExtraItemViews": {"staff": true, "admin": true}, "canSeeMedia": {"staff": true, "admin": true}, + "canSeeDocument": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canSeeSize": {"staff": true, "admin": true}, "canSeeSoftwareVersion": {"staff": true, "admin": true}, @@ -92,6 +96,257 @@ */ "clipLayers": ["transcripts", "keywords", "places", "events", "descriptions"], /* + "documentKeys" defines the metadata associated with each document. Required keys + are "*", "id" and "title". + A documentKey must have the following properties: + "id": The unique id of the key (as used by the server) + "title": The title of the key (as displayed by the client) + "type": Can be "boolean", "date", "enum", "float", "hue", "integer", + "layer", "string", "text", "time" or ["..."] (list of values of + this type). If type is "layer", this is a reference to the + annotations layer with the same id. + and can have any of the following properties: + "additionalSort": Ordered list of {key, operator} objects, where key is + another itemKey and operator is "+" or "-". This can be used to + override user.ui.listSort when results are sorted by this key. + "autocomplete": If true, the find element will provide autocomplete + "autocompleteSort": Sort order of autocomplete suggestions + "capability": A capability required to see data for this key + "columnRequired": If true, the column can't be removed from list view + "columnWidth": Default column width in px. If absent, no column for + this key can be added in list view. + "filter": If true, one can filter results by this key + "find": If true, this key will appear as an option in the find element + "flag": Can be "country" or "language". If set (and filter is true), a + flag icon corresponding to the field's value will be displayed. + "format": {type: string, args: [value, value, ...]}, used for special + formatting. This will invoke Ox.formatType(args). For details, see + https://oxjs.org/#doc/Ox.formatArea etc. + "secondaryId": If true, loading the URL "/value" will redirect to the + corresponding item, in case there is an exact match for this key + "sort": If true, one can sort results by this key + "sortOperator": Sort order ("+" or "-"), in case it differs from the + default for the key's type ("+" for strings, "-" for numbers) + "sortType": Special sort type ("person" or "title") which can be + further configured in "Manage Names" or "Manage Titles" + "value": {key: string, type: string} or {layer: string, type: string}, + for keys whose value is derived from other keys or layers (like + "number of actors" or "words per minute"). Possible values for type + are "length", "lengthperminute", "words", and "wordsperminute". + Alternatively, "value" can be set to the string "capability", which + results in an itemKey whose boolean value indicates the presence or + absence of a userLevel-dependent capability. This can be used to + create queries and lists like "all items this user can play" etc. + "values": [value, value, ...] Ordered list of values, in case "type" is + "enum" + */ + "documentKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "operator": "+", + "title": "Title", + "type": "string", + "find": true, + "sort": true, + "sortType": "title", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "type", + "operator": "+", + "title": "Type", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "author", + "operator": "+", + "title": "Author", + "type": ["string"], + "filter": true, + "find": true, + "sort": true, + "sortType": "person", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "publisher", + "operator": "+", + "title": "Publisher", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "place", + "title": "Place", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "date", + "title": "Date", + "type": "string", + "columnWidth": 120, + //"format": {"type": "date", "args": ["%a, %b %e, %Y"]}, + "sort": true + }, + { + "id": "series", + "title": "Series", + "type": "string", + "columnWidth": 128, + "find": true, + "sort": true + }, + { + "id": "edition", + "title": "Edition", + "type": "string", + "columnWidth": 128, + "find": true + }, + { + "id": "language", + "title": "Language", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "id", + "operator": "+", + "title": "ID", + "type": "string", + "sort": true, + "columnWidth": 64 + }, + { + "id": "extension", + "operator": "+", + "title": "Extension", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 64 + }, + { + "id": "dimensions", + "operator": "-", + "title": "Dimensions", + "type": "integer", + "sort": true, + "columnWidth": 128 + }, + { + "id": "size", + "operator": "-", + "title": "Size", + "type": "integer", + "sort": true, + "format": {"type": "value", "args": ["B"]}, + "columnWidth": 64 + }, + { + "id": "description", + "operator": "+", + "title": "Description", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "matches", + "operator": "-", + "title": "Matches", + "type": "integer", + "sort": true, + "columnWidth": 64 + }, + { + "id": "user", + "operator": "+", + "title": "User", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "created", + "operator": "-", + "title": "Created", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "modified", + "operator": "-", + "title": "Modified", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 150, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Out of Copyright", "Under Copyright", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + } + ], + /* "entities" can be used to store arbitrary data. They can be referenced in annotations, info view, or elsewhere. Each entry defines a specific class of entity object, its properties and their types (for example an "actor" @@ -882,16 +1137,27 @@ "calendarFind": "", "calendarSelection": "", "clipColumns": 2, + "collectionColumns": ["title", "id", "extension", "dimensions", "size", "description", "matches", "user", "created", "modified"], + "collectionColumnWidth": {}, + "collectionSelection": [], + "collectionSort": [ + {"key": "title", "operator": "+"}, + {"key": "extension", "operator": "+"} + ], + "collectionView": "grid", + "collections": {}, "columns": { "Colors": { - "columns": ["title", "source", "project", "language", "hue", "saturation", "brightness"], + "columns": ["title", "director", "language", "hue", "saturation", "brightness"], "columnWidth": {} } }, + "document": "", "documents": {}, "documentSize": 256, + "documentView": "view", "documentsSelection": {}, - "documentsSort": [{"key": "name", "operator": "+"}], + "documentsSort": [{"key": "title", "operator": "+"}], "documentsView": "grid", "edit": "", "edits": {}, @@ -917,6 +1183,7 @@ ], "filtersSize": 176, "find": {"conditions": [], "operator": "&"}, + "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", "icons": "frames", @@ -937,7 +1204,6 @@ "page": "", "part": { "api": "", - "documents": "", "entities": "", "faq": "", "help": "", @@ -981,7 +1247,7 @@ "featured": true, "volumes": true }, - "texts": { + "documents": { "personal": true, "favorite": true, "featured": true diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index 3a29ae96..a6df8ca7 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -39,11 +39,13 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. */ "capabilities": { "canAddItems": {"member": true, "staff": true, "admin": true}, + "canAddDocuments": {"member": true, "staff": true, "admin": true}, "canDownloadVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canEditAnnotations": {"staff": true, "admin": true}, "canEditDocuments": {"staff": true, "admin": true}, "canEditEntities": {"staff": true, "admin": true}, "canEditEvents": {"staff": true, "admin": true}, + "canEditFeaturedCollections": {"staff": true, "admin": true}, "canEditFeaturedEdits": {"staff": true, "admin": true}, "canEditFeaturedLists": {"staff": true, "admin": true}, "canEditFeaturedTexts": {"staff": true, "admin": true}, @@ -64,12 +66,14 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "canPlayClips": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canPlayVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canReadText": {"guest": 0, "member": 0, "staff": 1, "admin": 1}, - "canRemoveItems": {"admin": true}, + "canRemoveItems": {"staff": true, "admin": true}, + "canRemoveDocuments": {"staff": true, "admin": true}, "canSeeAccessed": {"staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"staff": true, "admin": true}, "canSeeExtraItemViews": {"staff": true, "admin": true}, "canSeeMedia": {"staff": true, "admin": true}, + "canSeeDocument": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, "canSeeSize": {"staff": true, "admin": true}, "canSeeSoftwareVersion": {"staff": true, "admin": true}, @@ -96,6 +100,257 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. */ "clipLayers": ["publicnotes", "keywords", "subtitles"], /* + "documentKeys" defines the metadata associated with each document. Required keys + are "*", "id" and "title". + A documentKey must have the following properties: + "id": The unique id of the key (as used by the server) + "title": The title of the key (as displayed by the client) + "type": Can be "boolean", "date", "enum", "float", "hue", "integer", + "layer", "string", "text", "time" or ["..."] (list of values of + this type). If type is "layer", this is a reference to the + annotations layer with the same id. + and can have any of the following properties: + "additionalSort": Ordered list of {key, operator} objects, where key is + another itemKey and operator is "+" or "-". This can be used to + override user.ui.listSort when results are sorted by this key. + "autocomplete": If true, the find element will provide autocomplete + "autocompleteSort": Sort order of autocomplete suggestions + "capability": A capability required to see data for this key + "columnRequired": If true, the column can't be removed from list view + "columnWidth": Default column width in px. If absent, no column for + this key can be added in list view. + "filter": If true, one can filter results by this key + "find": If true, this key will appear as an option in the find element + "flag": Can be "country" or "language". If set (and filter is true), a + flag icon corresponding to the field's value will be displayed. + "format": {type: string, args: [value, value, ...]}, used for special + formatting. This will invoke Ox.formatType(args). For details, see + https://oxjs.org/#doc/Ox.formatArea etc. + "secondaryId": If true, loading the URL "/value" will redirect to the + corresponding item, in case there is an exact match for this key + "sort": If true, one can sort results by this key + "sortOperator": Sort order ("+" or "-"), in case it differs from the + default for the key's type ("+" for strings, "-" for numbers) + "sortType": Special sort type ("person" or "title") which can be + further configured in "Manage Names" or "Manage Titles" + "value": {key: string, type: string} or {layer: string, type: string}, + for keys whose value is derived from other keys or layers (like + "number of actors" or "words per minute"). Possible values for type + are "length", "lengthperminute", "words", and "wordsperminute". + Alternatively, "value" can be set to the string "capability", which + results in an itemKey whose boolean value indicates the presence or + absence of a userLevel-dependent capability. This can be used to + create queries and lists like "all items this user can play" etc. + "values": [value, value, ...] Ordered list of values, in case "type" is + "enum" + */ + "documentKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "operator": "+", + "title": "Title", + "type": "string", + "find": true, + "sort": true, + "sortType": "title", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "type", + "operator": "+", + "title": "Type", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "author", + "operator": "+", + "title": "Author", + "type": ["string"], + "filter": true, + "find": true, + "sort": true, + "sortType": "person", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "publisher", + "operator": "+", + "title": "Publisher", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "place", + "title": "Place", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "date", + "title": "Date", + "type": "string", + "columnWidth": 120, + //"format": {"type": "date", "args": ["%a, %b %e, %Y"]}, + "sort": true + }, + { + "id": "series", + "title": "Series", + "type": "string", + "columnWidth": 128, + "find": true, + "sort": true + }, + { + "id": "edition", + "title": "Edition", + "type": "string", + "columnWidth": 128, + "find": true + }, + { + "id": "language", + "title": "Language", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "id", + "operator": "+", + "title": "ID", + "type": "string", + "sort": true, + "columnWidth": 64 + }, + { + "id": "extension", + "operator": "+", + "title": "Extension", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 64 + }, + { + "id": "dimensions", + "operator": "-", + "title": "Dimensions", + "type": "integer", + "sort": true, + "columnWidth": 128 + }, + { + "id": "size", + "operator": "-", + "title": "Size", + "type": "integer", + "sort": true, + "format": {"type": "value", "args": ["B"]}, + "columnWidth": 64 + }, + { + "id": "description", + "operator": "+", + "title": "Description", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "matches", + "operator": "-", + "title": "Matches", + "type": "integer", + "sort": true, + "columnWidth": 64 + }, + { + "id": "user", + "operator": "+", + "title": "User", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "created", + "operator": "-", + "title": "Created", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "modified", + "operator": "-", + "title": "Modified", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 150, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Out of Copyright", "Under Copyright", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + } + ], + /* "entities" can be used to store arbitrary data. They can be referenced in annotations, info view, or elsewhere. Each entry defines a specific class of entity object, its properties and their types (for example an "actor" @@ -822,16 +1077,27 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "calendarFind": "", "calendarSelection": "", "clipColumns": 2, + "collectionColumns": ["title", "id", "extension", "dimensions", "size", "description", "matches", "user", "created", "modified"], + "collectionColumnWidth": {}, + "collectionSelection": [], + "collectionSort": [ + {"key": "title", "operator": "+"}, + {"key": "extension", "operator": "+"} + ], + "collectionView": "grid", + "collections": {}, "columns": { "Colors": { "columns": ["title", "director", "language", "hue", "saturation", "brightness"], "columnWidth": {} } }, + "document": "", "documents": {}, "documentSize": 256, + "documentView": "view", "documentsSelection": {}, - "documentsSort": [{"key": "name", "operator": "+"}], + "documentsSort": [{"key": "title", "operator": "+"}], "documentsView": "grid", "edit": "", "edits": {}, @@ -857,6 +1123,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. ], "filtersSize": 176, "find": {"conditions": [], "operator": "&"}, + "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", "icons": "posters", @@ -877,7 +1144,6 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "page": "", "part": { "api": "", - "documents": "", "entities": "", "faq": "", "help": "", @@ -920,7 +1186,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "featured": true, "volumes": true }, - "texts": { + "documents": { "personal": true, "favorite": true, "featured": true diff --git a/pandora/document/management/__init__.py b/pandora/document/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/document/management/commands/__init__.py b/pandora/document/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/document/management/commands/rebuild_documentfind.py b/pandora/document/management/commands/rebuild_documentfind.py new file mode 100644 index 00000000..05af9d84 --- /dev/null +++ b/pandora/document/management/commands/rebuild_documentfind.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import print_function + +from django.core.management.base import BaseCommand +from django.db import connection, transaction +from django.db.models import fields +from django.conf import settings + +settings.RELOAD_CONFIG = False +import app.monkey_patch +from ... import models + +class Command(BaseCommand): + help = 'update document find and sort values' + args = '' + + def handle(self, **options): + ids = [i['id'] for i in models.Document.objects.all().values('id')] + for id in ids: + try: + i = models.Document.objects.get(id=id) + if i.file: + i.get_info() + i.get_ratio() + #print(i, i.ratio) + i.save() + except: + pass diff --git a/pandora/document/managers.py b/pandora/document/managers.py index 1530bde8..21d11947 100644 --- a/pandora/document/managers.py +++ b/pandora/document/managers.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 +import unicodedata + +from six import string_types from django.db.models import Q, Manager +from django.conf import settings import ox from oxdjango.query import QuerySet @@ -8,14 +12,31 @@ from oxdjango.query import QuerySet import entity.managers from oxdjango.managers import get_operator +from documentcollection.models import Collection +from item import utils + keymap = { - 'user': 'user__username', 'item': 'items__public_id', } -default_key = 'name' +default_key = 'title' -def parseCondition(condition, user, item=None): +def get_key_type(k): + key_type = (utils.get_by_id(settings.CONFIG['documentKeys'], k) or {'type': 'string'}).get('type') + if isinstance(key_type, list): + key_type = key_type[0] + key_type = { + 'title': 'string', + 'person': 'string', + 'text': 'string', + 'year': 'string', + 'length': 'string', + 'layer': 'string', + 'list': 'list', + }.get(key_type, key_type) + return key_type + +def parseCondition(condition, user, item=None, owner=None): ''' ''' k = condition.get('key', default_key) @@ -33,17 +54,47 @@ def parseCondition(condition, user, item=None): op = '=' if op.startswith('!'): - return ~buildCondition(k, op[1:], v) + return buildCondition(k, op[1:], v, user, True, owner=owner) else: - return buildCondition(k, op, v) + return buildCondition(k, op, v, user, owner=owner) - -def buildCondition(k, op, v): +def buildCondition(k, op, v, user, exclude=False, owner=None): import entity.models + from . import models + + # fixme: frontend should never call with list + if k == 'list': + print('fixme: frontend should never call with list', k, op, v) + k = 'collection' + + key_type = get_key_type(k) + facet_keys = models.Document.facet_keys if k == 'id': v = ox.fromAZ(v) - return Q(**{k: v}) - if isinstance(v, bool): + q = Q(**{k: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif k == 'groups': + if op == '==' and v == '$my': + if not owner: + owner = user + groups = owner.groups.all() + else: + key = 'name' + get_operator(op) + groups = Group.objects.filter(**{key: v}) + if not groups.count(): + return Q(id=0) + q = Q(groups__in=groups) + if exclude: + q = ~q + return q + elif k in ('oshash', 'items__public_id'): + q = Q(**{k: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif isinstance(v, bool): key = k elif k == 'entity': entity_key, entity_v = entity.managers.namePredicate(op, v) @@ -51,13 +102,87 @@ def buildCondition(k, op, v): v = entity.models.DocumentProperties.objects.filter(**{ 'entity__' + entity_key: entity_v }).values_list('document_id', flat=True) - else: - key = k + get_operator(op, 'istr') + elif k == 'collection': + q = Q(id=0) + l = v.split(":", 1) + if len(l) >= 2: + lqs = list(Collection.objects.filter(name=l[1], user__username=l[0])) + if len(lqs) == 1 and lqs[0].accessible(user): + l = lqs[0] + if l.query.get('static', False) is False: + data = l.query + q = parseConditions(data.get('conditions', []), + data.get('operator', '&'), + user, owner=l.user) + else: + q = Q(id__in=l.documents.all()) + else: + q = Q(id=0) + return q + elif key_type == 'boolean': + q = Q(**{'find__key': k, 'find__value': v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == "string": + in_find = True + if in_find: + value_key = 'find__value' + else: + value_key = k + if isinstance(v, string_types): + v = unicodedata.normalize('NFKD', v).lower() + if k in facet_keys: + in_find = False + facet_value = 'facets__value' + get_operator(op, 'istr') + v = models.Document.objects.filter(**{'facets__key': k, facet_value: v}) + value_key = 'id__in' + else: + value_key = value_key + get_operator(op) + k = str(k) + value_key = str(value_key) + if k == '*': + q = Q(**{value_key: v}) + elif in_find: + q = Q(**{'find__key': k, value_key: v}) + else: + q = Q(**{value_key: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == 'date': + def parse_date(d): + while len(d) < 3: + d.append(1) + return datetime(*[int(i) for i in d]) + + #using sort here since find only contains strings + v = parse_date(v.split('-')) + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q + else: # integer, float, list, time + #use sort table here + if key_type == 'time': + v = int(utils.parse_time(v)) + + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q key = str(key) - return Q(**{key: v}) + q = Q(**{key: v}) + if exclude: + q = ~q + return q -def parseConditions(conditions, operator, user, item=None): +def parseConditions(conditions, operator, user, item=None, owner=None): ''' conditions: [ { @@ -80,12 +205,12 @@ def parseConditions(conditions, operator, user, item=None): for condition in conditions: if 'conditions' in condition: q = parseConditions(condition['conditions'], - condition.get('operator', '&'), user, item) + condition.get('operator', '&'), user, item, owner=owner) if q: conn.append(q) pass else: - conn.append(parseCondition(condition, user, item)) + conn.append(parseCondition(condition, user, item, owner=owner)) if conn: q = conn[0] for c in conn[1:]: @@ -133,4 +258,21 @@ class DocumentManager(Manager): if conditions: qs = qs.filter(conditions) + #anonymous can only see public items + if not user or user.is_anonymous(): + level = 'guest' + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + qs = qs.filter(rightslevel__lte=allowed_level) + rendered_q = Q(rendered=True) + #users can see public items, there own items and items of there groups + else: + level = user.profile.get_level() + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + q = Q(rightslevel__lte=allowed_level) | Q(user=user) + rendered_q = Q(rendered=True) | Q(user=user) + if user.groups.count(): + q |= Q(groups__in=user.groups.all()) + rendered_q |= Q(groups__in=user.groups.all()) + qs = qs.filter(q) + return qs diff --git a/pandora/document/migrations/0003_new_fields.py b/pandora/document/migrations/0003_new_fields.py new file mode 100644 index 00000000..99f42f3c --- /dev/null +++ b/pandora/document/migrations/0003_new_fields.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-04 16:31 +from __future__ import unicode_literals + +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0002_auto_20160219_1537'), + ] + + operations = [ + migrations.AddField( + model_name='document', + name='data', + field=oxdjango.fields.DictField(default={}), + ), + migrations.AddField( + model_name='document', + name='rightslevel', + field=models.IntegerField(db_index=True, default=0), + ), + ] diff --git a/pandora/document/migrations/0004_migrate_text.py b/pandora/document/migrations/0004_migrate_text.py new file mode 100644 index 00000000..e965c0da --- /dev/null +++ b/pandora/document/migrations/0004_migrate_text.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os +import ox + +from django.db import migrations, models +from django.db.models import Max +from PIL import Image + +import oxdjango.fields + + +def migrate_texts(apps, schema_editor): + import os + import ox + import shutil + + import document.models + from document import utils + + Text = apps.get_model("text", "Text") + Document = apps.get_model("document", "Document") + Collection = apps.get_model("documentcollection", "Collection") + CollectionDocument = apps.get_model("documentcollection", "CollectionDocument") + User = apps.get_model("auth", "User") + + def add(self, document): + q = self.documents.filter(id=document.id) + if q.count() == 0: + l = CollectionDocument() + l.collection = self + l.document = document + l.index = CollectionDocument.objects.filter(collection=self).aggregate(Max('index'))['index__max'] + if l.index is None: + l.index = 0 + else: + l.index += 1 + l.save() + + def path(self, name=''): + h = ox.toAZ(self.id) + h = (7-len(h))*'0' + h + return os.path.join('documents', h[:2], h[2:4], h[4:6], h[6:], name) + + def update_info(self): + pdf = self.file.path + page = 1 + image = os.path.join(os.path.dirname(pdf), '1024p%d.jpg' % page) + utils.extract_pdfpage(pdf, image, page) + self.pages = utils.pdfpages(self.file.path) + if os.path.exists(image): + size = Image.open(image).size + self.ratio = size[0] / size[1] + + if Text.objects.filter(status='featured').count(): + first_user = User.objects.all()[0] + featured, created = Collection.objects.get_or_create(user=first_user, name='Featured Texts') + if created: + featured.status = 'featured' + featured.save() + + for t in Text.objects.all(): + d = Document() + d.extension = t.type + if t.name == '': + d.name = 'Index' + else: + d.name = t.name + d.user = t.user + d.description = t.description + d.data['text'] = t.text + d.data['embeds'] = t.embeds + d.save() + if t.type == 'pdf': + d.file.name = path(d, 'data.pdf') + os.makedirs(os.path.dirname(d.file.path)) + shutil.copy2(t.file.path, d.file.path) + d.oshash = ox.oshash(d.file.path) + update_info(d) + d.save() + Document.objects.filter(id=d.id).update(created=t.created, modified=t.modified) + c, created = Collection.objects.get_or_create(user=t.user, name='Texts') + add(c, d) + if t.status == 'featured': + add(featured, d) + for user in t.subscribed_users.all(): + favorite, created = Collection.objects.get_or_create(user=user, name='Favorite Texts') + add(favorite, d) + + ''' + for d in document.models.Document.objects.filter(id__in=fix_info): + d.get_info() + d.get_ratio() + d.save() + ''' + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '__first__'), + ('text', '__first__'), + ('document', '0003_new_fields'), + ('documentcollection', '0001_initial'), + ] + + operations = [ + migrations.RunPython(migrate_texts), + ] diff --git a/pandora/document/migrations/0005_auto_20161008_1232.py b/pandora/document/migrations/0005_auto_20161008_1232.py new file mode 100644 index 00000000..ba3acb7a --- /dev/null +++ b/pandora/document/migrations/0005_auto_20161008_1232.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-08 12:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('document', '0004_migrate_text'), + ] + + operations = [ + migrations.CreateModel( + name='Access', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('access', models.DateTimeField(auto_now=True)), + ('accessed', models.IntegerField(default=0)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accessed', to='document.Document')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accessed_documents', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='access', + unique_together=set([('document', 'user')]), + ), + ] diff --git a/pandora/document/migrations/0006_auto_20161026_1259.py b/pandora/document/migrations/0006_auto_20161026_1259.py new file mode 100644 index 00000000..799471da --- /dev/null +++ b/pandora/document/migrations/0006_auto_20161026_1259.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-26 12:59 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0005_auto_20161008_1232'), + ] + + operations = [ + migrations.CreateModel( + name='Find', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, max_length=200)), + ('value', models.TextField(blank=True, db_index=True)), + ], + ), + migrations.CreateModel( + name='Sort', + fields=[ + ('document', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='sort', serialize=False, to='document.Document')), + ('created', models.DateTimeField(blank=True, db_index=True, null=True)), + ('name', models.CharField(db_index=True, max_length=1000, null=True)), + ('id', models.CharField(db_index=True, max_length=1000, null=True)), + ('extension', models.CharField(db_index=True, max_length=1000, null=True)), + ('dimensions', models.BigIntegerField(blank=True, db_index=True, null=True)), + ('size', models.BigIntegerField(blank=True, db_index=True, null=True)), + ('description', models.CharField(db_index=True, max_length=1000, null=True)), + ('matches', models.BigIntegerField(blank=True, db_index=True, null=True)), + ('user', models.CharField(db_index=True, max_length=1000, null=True)), + ('modified', models.DateTimeField(blank=True, db_index=True, null=True)), + ('accessed', models.DateTimeField(blank=True, db_index=True, null=True)), + ('timesaccessed', models.BigIntegerField(blank=True, db_index=True, null=True)), + ('rightslevel', models.BigIntegerField(blank=True, db_index=True, null=True)), + ], + ), + migrations.AddField( + model_name='find', + name='document', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='find', to='document.Document'), + ), + migrations.AlterUniqueTogether( + name='find', + unique_together=set([('document', 'key')]), + ), + ] diff --git a/pandora/document/migrations/0007_auto_20161026_1559.py b/pandora/document/migrations/0007_auto_20161026_1559.py new file mode 100644 index 00000000..3efd4d7c --- /dev/null +++ b/pandora/document/migrations/0007_auto_20161026_1559.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-26 15:59 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +def migrate_data(apps, schema_editor): + Document = apps.get_model('document', 'Document') + for d in Document.objects.all(): + if 'title' not in d.data: + d.data['title'] = d.name + if 'description' not in d.data: + d.data['description'] = d.description + d.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0006_auto_20161026_1259'), + ] + + operations = [ + migrations.RunPython(migrate_data), + migrations.AlterField( + model_name='document', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='document', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='document', + name='description_sort', + ), + migrations.RemoveField( + model_name='document', + name='dimensions_sort', + ), + migrations.RemoveField( + model_name='document', + name='name_sort', + ), + migrations.RemoveField( + model_name='document', + name='name', + ), + migrations.RemoveField( + model_name='document', + name='description', + ), + ] diff --git a/pandora/document/migrations/0008_auto_20161026_1625.py b/pandora/document/migrations/0008_auto_20161026_1625.py new file mode 100644 index 00000000..5164c302 --- /dev/null +++ b/pandora/document/migrations/0008_auto_20161026_1625.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-26 16:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0007_auto_20161026_1559'), + ] + + operations = [ + migrations.CreateModel( + name='Facet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, max_length=200)), + ('value', models.CharField(db_index=True, max_length=1000)), + ('sortvalue', models.CharField(db_index=True, max_length=1000)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='facets', to='document.Document')), + ], + ), + migrations.AlterUniqueTogether( + name='facet', + unique_together=set([('document', 'key', 'value')]), + ), + ] diff --git a/pandora/document/migrations/0009_add_group.py b/pandora/document/migrations/0009_add_group.py new file mode 100644 index 00000000..717d24c4 --- /dev/null +++ b/pandora/document/migrations/0009_add_group.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-27 12:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '__first__'), + ('document', '0008_auto_20161026_1625'), + ] + + operations = [ + migrations.AddField( + model_name='document', + name='groups', + field=models.ManyToManyField(blank=True, related_name='documents', to='auth.Group'), + ), + ] diff --git a/pandora/document/models.py b/pandora/document/models.py index 54bb30d0..33c652d2 100644 --- a/pandora/document/models.py +++ b/pandora/document/models.py @@ -5,17 +5,23 @@ from __future__ import division, print_function, absolute_import import os import re from glob import glob +import unicodedata from six import string_types from six.moves.urllib.parse import quote, unquote -from django.db import models -from django.db.models import Max -from django.contrib.auth.models import User +from django.db import models, transaction +from django.db.models import Q, Sum, Max +from django.contrib.auth.models import User, Group from django.db.models.signals import pre_delete +from django.conf import settings from PIL import Image import ox + +from oxdjango import fields +from oxdjango.sortmodel import get_sort_field +from person.models import get_name_sort from item.models import Item from archive.extract import resize_image from archive.chunk import save_chunk @@ -23,57 +29,249 @@ from archive.chunk import save_chunk from . import managers from . import utils -def get_path(f, x): return f.path(x) + +def get_path(f, x): + return f.path(x) class Document(models.Model): - class Meta: - unique_together = ("user", "name", "extension") - created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) - user = models.ForeignKey(User, related_name='files') - name = models.CharField(max_length=255) + user = models.ForeignKey(User, related_name='documents') + groups = models.ManyToManyField(Group, blank=True, related_name='documents') + extension = models.CharField(max_length=255) size = models.IntegerField(default=0) matches = models.IntegerField(default=0) - ratio = models.FloatField(default=1) + ratio = models.FloatField(default=640/1024) pages = models.IntegerField(default=-1) width = models.IntegerField(default=-1) height = models.IntegerField(default=-1) - description = models.TextField(default="") + oshash = models.CharField(max_length=16, unique=True, null=True) - file = models.FileField(default=None, blank=True,null=True, upload_to=get_path) + file = models.FileField(default=None, blank=True, null=True, upload_to=get_path) objects = managers.DocumentManager() - uploading = models.BooleanField(default = False) - - name_sort = models.CharField(max_length=255, null=True) - description_sort = models.CharField(max_length=512, null=True) - dimensions_sort = models.CharField(max_length=512) + uploading = models.BooleanField(default=False) items = models.ManyToManyField(Item, through='ItemProperties', related_name='documents') + rightslevel = models.IntegerField(db_index=True, default=0) + data = fields.DictField(default={}) + + def update_access(self, user): + if not user.is_authenticated(): + user = None + access, created = Access.objects.get_or_create(document=self, user=user) + if not created: + access.save() + + def update_facet(self, key): + current_values = self.get_value(key, []) + if key == 'name': + current_values = [] + for k in settings.CONFIG['documentKeys']: + if k.get('sortType') == 'person': + current_values += self.get(k['id'], []) + if not isinstance(current_values, list): + if not current_values: + current_values = [] + else: + current_values = [unicode(current_values)] + + filter_map = utils.get_by_id(settings.CONFIG['documentKeys'], key).get('filterMap') + if filter_map: + filter_map = re.compile(filter_map) + _current_values = [] + for value in current_values: + value = filter_map.findall(value) + if value: + _current_values.append(value[0]) + current_values = _current_values + + current_values = list(set(current_values)) + current_values = [ox.decode_html(ox.strip_tags(v)) for v in current_values] + current_values = [unicodedata.normalize('NFKD', v) for v in current_values] + self.update_facet_values(key, current_values) + + def update_facet_values(self, key, current_values): + current_sortvalues = set([value.lower() for value in current_values]) + saved_values = [i.value.lower() for i in Facet.objects.filter(document=self, key=key)] + removed_values = filter(lambda i: i not in current_sortvalues, saved_values) + + if removed_values: + q = Q() + for v in removed_values: + q |= Q(value__iexact=v) + Facet.objects.filter(document=self, key=key).filter(q).delete() + + for value in current_values: + if value.lower() not in saved_values: + sortvalue = value + if key in self.person_keys + ['name']: + sortvalue = get_name_sort(value) + sortvalue = utils.sort_string(sortvalue).lower()[:900] + f, created = Facet.objects.get_or_create(document=self, key=key, value=value, sortvalue=sortvalue) + if created: + Facet.objects.filter(document=self, key=key, value__iexact=value).exclude(value=value).delete() + Facet.objects.filter(key=key, value__iexact=value).exclude(value=value).update(value=value) + saved_values.append(value.lower()) + + def update_facets(self): + for key in set(self.facet_keys + ['title']): + self.update_facet(key) + + def update_find(self): + + def save(key, value): + if value not in ('', None): + f, created = Find.objects.get_or_create(document=self, key=key) + if isinstance(value, bool): + value = value and 'true' or 'false' + if isinstance(value, string_types): + value = ox.decode_html(ox.strip_tags(value.strip())) + value = unicodedata.normalize('NFKD', value).lower() + f.value = value + f.save() + else: + Find.objects.filter(document=self, key=key).delete() + + with transaction.atomic(): + data = self.json() + for key in settings.CONFIG['documentKeys']: + i = key['id'] + if i == 'rightslevel': + save(i, self.rightslevel) + elif i not in ('*', 'dimensions') and i not in self.facet_keys: + value = data.get(i) + if isinstance(value, list): + value = u'\n'.join(value) + save(i, value) + + base_keys = ('id', 'size', 'dimensions', 'extension', 'matches') + + def update_sort(self): + try: + s = self.sort + except Sort.DoesNotExist: + s = Sort(document=self) + + s.id = self.id + s.extension = self.extension + s.size = self.size + s.matches = self.matches + if self.extension == 'pdf': + s.dimensions = ox.sort_string('2') + ox.sort_string('%d' % self.pages) + else: + if self.extension == 'html': + resolution_sort = self.dimensions + s.dimensions = ox.sort_string('1') + ox.sort_string('%d' % resolution_sort) + else: + resolution_sort = self.width * self.height + s.dimensions = ox.sort_string('0') + ox.sort_string('%d' % resolution_sort) + + def sortNames(values): + sort_value = u'' + if values: + sort_value = u'; '.join([get_name_sort(name) for name in values]) + if not sort_value: + sort_value = u'' + return sort_value + + def set_value(s, name, value): + if isinstance(value, string_types): + value = ox.decode_html(value.lower()) + if not value: + value = None + setattr(s, name, value) + + def get_value(source, key): + if 'value' in key and 'layer' in key['value']: + value = [a.value for a in self.annotations.filter(layer=key['value']['layer']).exclude(value='')] + else: + value = self.get_value(source) + return value + + def get_words(source, key): + value = get_value(source, key) + if isinstance(value, list): + value = '\n'.join(value) + value = len(value.split(' ')) if value else 0 + return value + + for key in filter(lambda k: k.get('sort', False), settings.CONFIG['documentKeys']): + name = key['id'] + if name not in self.base_keys: + source = name + sort_type = key.get('sortType', key['type']) + if 'value' in key: + if 'key' in key['value']: + source = key['value']['key'] + sort_type = key['value'].get('type', sort_type) + if isinstance(sort_type, list): + sort_type = sort_type[0] + if sort_type == 'title': + value = self.get_value(source, u'Untitled') + value = utils.sort_title(value)[:955] + set_value(s, name, value) + elif sort_type == 'person': + value = sortNames(self.get_value(source, [])) + value = utils.sort_string(value)[:955] + set_value(s, name, value) + elif sort_type == 'string': + value = self.get_value(source, u'') + if isinstance(value, list): + value = u','.join(value) + value = utils.sort_string(value)[:955] + set_value(s, name, value) + elif sort_type == 'words': + value = get_words(source, key) if s.duration else None + set_value(s, name, value) + elif sort_type == 'wordsperminute': + value = get_words(source, key) + value = value / (s.duration / 60) if value and s.duration else None + set_value(s, name, value) + elif sort_type in ('length', 'integer', 'time', 'float'): + # can be length of strings or length of arrays, i.e. keywords + if 'layer' in key.get('value', []): + value = self.annotations.filter(layer=key['value']['layer']).count() + else: + value = self.get_value(source) + if isinstance(value, list): + value = len(value) + set_value(s, name, value) + elif sort_type == 'year': + value = self.get_value(source) + set_value(s, name, value) + elif sort_type == 'date': + value = self.get_value(source) + if isinstance(value, string_types): + value = datetime_safe.datetime.strptime(value, '%Y-%m-%d') + set_value(s, name, value) + s.save() + def save(self, *args, **kwargs): if not self.uploading: if self.file: self.size = self.file.size self.get_info() - - self.name_sort = ox.sort_string(self.name or u'')[:255].lower() - if self.description: - self.description_sort = ox.sort_string(self.description)[:512].lower() + if self.extension == 'html': + self.size = len(self.data.get('text', '')) + + if self.id: + self.update_sort() + self.update_find() + self.update_facets() + new = False else: - self.description_sort = None - if self.extension == 'pdf': - self.dimensions_sort = ox.sort_string('1') + ox.sort_string('%d' % self.pages) - else: - resolution_sort = self.width * self.height - self.dimensions_sort = ox.sort_string('0') + ox.sort_string('%d' % resolution_sort) - + new = True super(Document, self).save(*args, **kwargs) + if new: + self.update_sort() + self.update_find() + self.update_facets() self.update_matches() def __unicode__(self): @@ -100,40 +298,61 @@ class Document(models.Model): def get_id(self): return ox.toAZ(self.id) + def accessible(self, user): + return self.user == user or self.status in ('public', 'featured') + def editable(self, user, item=None): if not user or user.is_anonymous(): return False if self.user == user or \ user.is_staff or \ - user.profile.capability('canEditDocuments') == True or \ + user.profile.capability('canEditDocuments') is True or \ (item and item.editable(user)): return True return False def edit(self, data, user, item=None): - for key in data: - if key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() - if not data['name']: - data['name'] = "Untitled" - name = data['name'] - num = 1 - while Document.objects.filter(name=name, user=self.user, extension=self.extension).exclude(id=self.id).count()>0: - num += 1 - name = data['name'] + ' [%d]' % num - self.name = name - elif key == 'description' and not item: - self.description = ox.sanitize_html(data['description']) if item: p, created = ItemProperties.objects.get_or_create(item=item, document=self) if 'description' in data: p.description = ox.sanitize_html(data['description']) p.save() + else: + for key in data: + k = list(filter(lambda i: i['id'] == key, settings.CONFIG['documentKeys'])) + ktype = k and k[0].get('type') or '' + if key == 'text' and self.extension == 'html': + self.data['text'] = ox.sanitize_html(data['text'], global_attributes=[ + 'data-name', + 'data-type', + 'data-value', + 'lang' + ]) + elif ktype == 'text': + self.data[key] = ox.sanitize_html(data[key]) + elif ktype == '[text]': + self.data[key] = [ox.sanitize_html(t) for t in data[key]] + elif ktype == '[string]': + self.data[key] = [ox.escape_html(t) for t in data[key]] + elif isinstance(data[key], string_types): + self.data[key] = ox.escape_html(data[key]) + elif isinstance(data[key], list): + def cleanup(i): + if isinstance(i, string_types): + i = ox.escape_html(i) + return i + self.data[key] = [cleanup(i) for i in data[key]] + elif isinstance(data[key], int) or isinstance(data[key], float): + self.data[key] = data[key] + else: + self.data[key] = ox.escape_html(data[key]) @property def dimensions(self): if self.extension == 'pdf': return self.pages + elif self.extension == 'html': + return len(self.data.get('text', '').split(' ')) else: return self.resolution @@ -141,21 +360,43 @@ class Document(models.Model): def resolution(self): return [self.width, self.height] + def get_value(self, key, default=None): + if key in ( + 'extension', + 'id', + 'matches', + 'ratio', + 'size', + ): + return getattr(self, key) + elif key == 'user': + return self.user.username + else: + return self.data.get(key, default) + def json(self, keys=None, user=None, item=None): if not keys: - keys=[ + keys = [ 'description', 'dimensions', 'editable', 'entities', 'extension', 'id', - 'name', 'oshash', + 'title', 'ratio', + 'matches', 'size', 'user', ] + if self.extension in ('html', 'txt'): + keys.append('text') + for key in settings.CONFIG['documentKeys']: + if key['id'] in ('*', ): + continue + if key['id'] not in keys: + keys.append(key['id']) response = {} _map = { } @@ -166,6 +407,10 @@ class Document(models.Model): response[key] = self.editable(user) elif key == 'user': response[key] = self.user.username + elif key == 'accessed': + response[key] = self.accessed.aggregate(Max('access'))['access__max'] + elif key == 'timesaccessed': + response[key] = self.accessed.aggregate(Sum('accessed'))['accessed__sum'] elif key == 'entities': dps = self.documentproperties.select_related('entity').order_by('index') response[key] = entity_jsons = [] @@ -175,8 +420,12 @@ class Document(models.Model): entity_jsons.append(entity_json) elif key == 'items': response[key] = [i['public_id'] for i in self.items.all().values('public_id')] + elif key in self.data: + response[key] = self.data[key] elif hasattr(self, _map.get(key, key)): - response[key] = getattr(self, _map.get(key,key)) or '' + response[key] = getattr(self, _map.get(key, key)) or '' + if self.extension == 'html': + response['text'] = self.data.get('text', '') if item: if isinstance(item, string_types): item = Item.objects.get(public_id=item) @@ -185,6 +434,10 @@ class Document(models.Model): if 'description' in keys and d[0].description: response['description'] = d[0].description response['index'] = d[0].index + if keys: + for key in list(response): + if key not in keys: + del response[key] return response def path(self, name=''): @@ -211,6 +464,9 @@ class Document(models.Model): return False, 0 def thumbnail(self, size=None, page=None): + if not self.file: + return os.path.join(settings.STATIC_ROOT, 'png/cover.png') + return os.path.join(settings.STATIC_ROOT, 'jpg/list256.jpg') src = self.file.path folder = os.path.dirname(src) if size: @@ -278,12 +534,12 @@ class Document(models.Model): try: size = Image.open(image).size except: - size = [1,1] + size = [1, 1] else: if self.width > 0: size = self.resolution else: - size = [1,1] + size = [640, 1024] self.ratio = size[0] / size[1] return self.ratio @@ -337,6 +593,97 @@ class ItemProperties(models.Model): if self.description: self.description_sort = ox.sort_string(self.description)[:512].lower() else: - self.description_sort = self.document.description_sort + self.description_sort = self.document.sort.description super(ItemProperties, self).save(*args, **kwargs) + + +class Access(models.Model): + class Meta: + unique_together = ("document", "user") + + access = models.DateTimeField(auto_now=True) + document = models.ForeignKey(Document, related_name='accessed') + user = models.ForeignKey(User, null=True, related_name='accessed_documents') + accessed = models.IntegerField(default=0) + + def save(self, *args, **kwargs): + if not self.accessed: + self.accessed = 0 + self.accessed += 1 + super(Access, self).save(*args, **kwargs) + timesaccessed = Access.objects.filter(document=self.document).aggregate(Sum('accessed'))['accessed__sum'] + Sort.objects.filter(document=self.document).update(timesaccessed=timesaccessed, accessed=self.access) + + def __unicode__(self): + if self.user: + return u"%s/%s/%s" % (self.user, self.document, self.access) + return u"%s/%s" % (self.item, self.access) + +class Facet(models.Model): + ''' + used for keys that can have multiple values like people, languages etc. + does not perform to well if total number of items goes above 10k + this happens for keywords in 0xdb right now + ''' + + class Meta: + unique_together = ("document", "key", "value") + + document = models.ForeignKey('Document', related_name='facets') + key = models.CharField(max_length=200, db_index=True) + value = models.CharField(max_length=1000, db_index=True) + sortvalue = models.CharField(max_length=1000, db_index=True) + + def __unicode__(self): + return u"%s=%s" % (self.key, self.value) + + def save(self, *args, **kwargs): + if not self.sortvalue: + self.sortvalue = utils.sort_string(self.value).lower()[:900] + self.sotvalue = self.sortvalue.lower() + super(Facet, self).save(*args, **kwargs) + +Document.facet_keys = [] +for key in settings.CONFIG['documentKeys']: + if 'autocomplete' in key and 'autocompleteSortKey' not in key or \ + key.get('filter'): + Document.facet_keys.append(key['id']) + +Document.person_keys = [] +for key in settings.CONFIG['itemKeys']: + if key.get('sortType') == 'person': + Document.person_keys.append(key['id']) + +class Find(models.Model): + + class Meta: + unique_together = ('document', 'key') + + document = models.ForeignKey('Document', related_name='find', db_index=True) + key = models.CharField(max_length=200, db_index=True) + value = models.TextField(blank=True, db_index=settings.DB_GIN_TRGM) + + def __unicode__(self): + return u'%s=%s' % (self.key, self.value) + +''' +Sort +table constructed based on info in settings.CONFIG['documentKeys'] +''' +attrs = { + '__module__': 'document.models', + 'document': models.OneToOneField('Document', related_name='sort', primary_key=True), + 'created': models.DateTimeField(null=True, blank=True, db_index=True), +} +for key in filter(lambda k: k.get('sort', False) or k['type'] in ('integer', 'time', 'float', 'date', 'enum'), settings.CONFIG['documentKeys']): + name = key['id'] + sort_type = key.get('sortType', key['type']) + if isinstance(sort_type, list): + sort_type = sort_type[0] + field = get_sort_field(sort_type) + if name not in attrs: + attrs[name] = field[0](**field[1]) + +Sort = type('Sort', (models.Model,), attrs) +Sort.fields = [f.name for f in Sort._meta.fields] diff --git a/pandora/document/sync_sort.py b/pandora/document/sync_sort.py new file mode 100644 index 00000000..211fc588 --- /dev/null +++ b/pandora/document/sync_sort.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import print_function + +from django.core.management.base import BaseCommand +from django.db import connection, transaction +from django.db.models import fields +from django.conf import settings + +from . import models + +def update_tables(debug=False): + table_name = models.Sort._meta.db_table + cursor = connection.cursor() + db_rows = connection.introspection.get_table_description(cursor, table_name) + db_fields = dict([(row[0], row) for row in db_rows]) + db_types = dict([(row[0], + connection.introspection.data_types_reverse[row[1]]) for row in db_rows]) + + model_fields = ['document_id'] + [f.name for f in models.Sort._meta.fields] + rebuild = False + + changes = [] + for name in db_types: + if name not in model_fields: + sql = 'ALTER TABLE "%s" DROP COLUMN "%s"' % (table_name, name) + changes.append(sql) + + for f in models.Sort._meta.fields: + if not f.primary_key: + name = f.name + col_type = f.db_type(connection) + if name not in db_fields: + sql = 'ALTER TABLE "%s" ADD COLUMN "%s" %s' % (table_name, name, col_type) + changes.append(sql) + sql = 'CREATE INDEX "%s_%s_idx" ON "%s" ("%s")' % (table_name, name, + table_name, name) + changes.append(sql) + rebuild = True + elif f.__class__.__name__ != db_types[name]: + sql = 'ALTER TABLE "%s" DROP COLUMN "%s"' % (table_name, name) + changes.append(sql) + sql = 'ALTER TABLE "%s" ADD COLUMN "%s" %s' % (table_name, name, col_type) + changes.append(sql) + sql = 'CREATE INDEX "%s_%s_idx" ON "%s" ("%s")' % (table_name, name, + table_name, name) + changes.append(sql) + rebuild = True + elif db_types[name] == 'CharField' and db_fields[name][3] != f.max_length: + sql = 'ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (table_name, name, + col_type) + changes.append(sql) + sql = 'ALTER TABLE "%s" ALTER COLUMN "%s" %s NOT NULL' % (table_name, name, + f.null and "DROP" or "SET") + changes.append(sql) + rebuild = True + + if changes: + print("Updating document sort schema...") + for sql in changes: + if debug: + print(sql) + cursor.execute(sql) + transaction.commit() + if rebuild: + print("Updating document sort values...") + ids = [i['id'] for i in models.Document.objects.all().values('id')] + for id in ids: + d = models.Document.objects.get(pk=id) + if debug: + print(d) + d.update_sort() diff --git a/pandora/document/utils.py b/pandora/document/utils.py index e5f49345..a33afcda 100644 --- a/pandora/document/utils.py +++ b/pandora/document/utils.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 + import subprocess +from item.utils import sort_title, sort_string, get_by_id + def pdfpages(pdf): return int(pdfinfo(pdf).get('pages', '0')) diff --git a/pandora/document/views.py b/pandora/document/views.py index f4820438..fa950015 100644 --- a/pandora/document/views.py +++ b/pandora/document/views.py @@ -3,7 +3,9 @@ from __future__ import division, print_function, absolute_import import os +import re from glob import glob +import unicodedata from six import string_types import ox @@ -13,7 +15,8 @@ from oxdjango.decorators import login_required_json from oxdjango.http import HttpFileResponse from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson from django import forms -from django.db.models import Sum +from django.db.models import Count, Sum +from django.conf import settings from item import utils from item.models import Item @@ -35,6 +38,13 @@ def get_document_or_404_json(id): @login_required_json def addDocument(request, data): ''' + Create new html document + takes { + title: string + } + + or + Adds one or more documents to one or more items takes { item: string or [string], // one or more item ids (optional) @@ -46,46 +56,54 @@ def addDocument(request, data): see: editDocument, findDocuments, getDocument, removeDocument, sortDocuments ''' response = json_response() - if 'ids' in data: - ids = data['ids'] + if 'title' in data: + doc = models.Document(user=request.user, extension='html') + doc.data['title'] = data['title'] + doc.save() + response = json_response(status=200, text='created') + response['data'] = doc.json(user=request.user) + add_changelog(request, data, doc.get_id()) else: - ids = [data['id']] - if 'item' in data: - if isinstance(data['item'], string_types): - item = Item.objects.get(public_id=data['item']) - if item.editable(request.user): - for id in ids: - document = models.Document.get(id) - document.add(item) - add_changelog(request, data, item.public_id) - else: - response = json_response(status=403, text='permission denied') + if 'ids' in data: + ids = data['ids'] else: - for item in Item.objects.filter(public_id__in=data['item']): + ids = [data['id']] + if 'item' in data: + if isinstance(data['item'], string_types): + item = Item.objects.get(public_id=data['item']) if item.editable(request.user): for id in ids: document = models.Document.get(id) document.add(item) - add_changelog(request, data, data['item']) - elif 'entity' in data: - if isinstance(data['entity'], string_types): - entity = Entity.get(data['entity']) - if entity.editable(request.user): - for id in ids: - document = models.Document.get(id) - entity.add(document) - add_changelog(request, data, entity.get_id()) + add_changelog(request, data, item.public_id) + else: + response = json_response(status=403, text='permission denied') else: - response = json_response(status=403, text='permission denied') - else: - for entity in Entity.objects.filter(id__in=map(ox.fromAZ, data['entity'])): + for item in Item.objects.filter(public_id__in=data['item']): + if item.editable(request.user): + for id in ids: + document = models.Document.get(id) + document.add(item) + add_changelog(request, data, data['item']) + elif 'entity' in data: + if isinstance(data['entity'], string_types): + entity = Entity.get(data['entity']) if entity.editable(request.user): for id in ids: document = models.Document.get(id) entity.add(document) - add_changelog(request, data, data['entity']) - else: - response = json_response(status=500, text='invalid request') + add_changelog(request, data, entity.get_id()) + else: + response = json_response(status=403, text='permission denied') + else: + for entity in Entity.objects.filter(id__in=map(ox.fromAZ, data['entity'])): + if entity.editable(request.user): + for id in ids: + document = models.Document.get(id) + entity.add(document) + add_changelog(request, data, data['entity']) + else: + response = json_response(status=500, text='invalid request') return render_to_json_response(response) actions.register(addDocument, cache=False) @@ -95,7 +113,8 @@ def editDocument(request, data): Edits data for a document takes { id: string, // document id - name: string, // new document name + + key: value, // set new data description: string // new document description item: string // item id (optional) } @@ -126,22 +145,26 @@ actions.register(editDocument, cache=False) def _order_query(qs, sort, item=None): + prefix = 'sort__' order_by = [] for e in sort: operator = e['operator'] if operator != '-': operator = '' key = { - 'name': 'name_sort', 'description': 'descriptions__description_sort' - if item else 'description_sort', - 'dimensions': 'dimensions_sort', + if item else 'description', 'index': 'items__itemproperties__index', + #fixme: + 'position': 'id', + 'name': 'title', }.get(e['key'], e['key']) if key == 'resolution': order_by.append('%swidth'%operator) order_by.append('%sheight'%operator) else: + if '__' not in key: + key = "%s%s" % (prefix, key) order = '%s%s' % (operator, key) order_by.append(order) if order_by: @@ -149,6 +172,24 @@ def _order_query(qs, sort, item=None): qs = qs.distinct() return qs +def _order_by_group(query): + if 'sort' in query: + if len(query['sort']) == 1 and query['sort'][0]['key'] == 'items': + order_by = query['sort'][0]['operator'] == '-' and '-items' or 'items' + if query['group'] == "year": + secondary = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue' + order_by = (order_by, secondary) + elif query['group'] != "keyword": + order_by = (order_by, 'sortvalue') + else: + order_by = (order_by, 'value') + else: + order_by = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue' + order_by = (order_by, 'items') + else: + order_by = ('-sortvalue', 'items') + return order_by + def get_item(query): for c in query.get('conditions', []): if c.get('key') == 'item': @@ -162,7 +203,7 @@ def parse_query(data, user): for key in ('keys', 'group', 'file', 'range', 'position', 'positions', 'sort'): if key in data: query[key] = data[key] - query['qs'] = models.Document.objects.find(data, user).exclude(name='') + query['qs'] = models.Document.objects.find(data, user) query['item'] = get_item(data.get('query', {})) return query @@ -192,7 +233,24 @@ def findDocuments(request, data): #order qs = _order_query(query['qs'], query['sort'], query['item']) response = json_response() - if 'keys' in data: + if 'group' in query: + response['data']['items'] = [] + items = 'items' + document_qs = query['qs'] + order_by = _order_by_group(query) + qs = models.Facet.objects.filter(key=query['group']).filter(document__id__in=document_qs) + qs = qs.values('value').annotate(items=Count('id')).order_by(*order_by) + + if 'positions' in query: + response['data']['positions'] = {} + ids = [j['value'] for j in qs] + response['data']['positions'] = utils.get_positions(ids, query['positions']) + elif 'range' in data: + qs = qs[query['range'][0]:query['range'][1]] + response['data']['items'] = [{'name': i['value'], 'items': i[items]} for i in qs] + else: + response['data']['items'] = qs.count() + elif 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] response['data']['items'] = [l.json(data['keys'], request.user, query['item']) for l in qs] @@ -330,23 +388,15 @@ def upload(request): if 'chunk' in request.FILES: if file.editable(request.user): response = process_chunk(request, file.save_chunk) - response['resultUrl'] = request.build_absolute_uri(file.get_absolute_url()) + response['resultUrl'] = file.get_absolute_url() # id is used to select document in dialog after upload response['id'] = file.get_id() return render_to_json_response(response) #init upload else: if not file: - created = False - num = 1 - _name = name - while not created: - file, created = models.Document.objects.get_or_create( - user=request.user, name=name, extension=extension) - if not created: - num += 1 - name = _name + ' [%d]' % num - file.name = name + file = models.Document(user=request.user, extension=extension) + file.data['title'] = name file.extension = extension file.uploading = True file.save() @@ -361,10 +411,81 @@ def upload(request): file.width = -1 file.pages = -1 file.save() - upload_url = request.build_absolute_uri('/api/upload/document?id=%s' % file.get_id()) + upload_url = '/api/upload/document?id=%s' % file.get_id() return render_to_json_response({ 'uploadUrl': upload_url, - 'url': request.build_absolute_uri(file.get_absolute_url()), + 'url': file.get_absolute_url(), 'result': 1 }) return render_to_json_response(response) + +def autocompleteDocuments(request, data): + ''' + Returns autocomplete strings for a given documeny key and search string + takes { + key: string, // document key + value: string, // search string + operator: string, // '=', '==', '^', '$' + query: object, // document query to limit results, see `find` + range: [int, int] // range of results to return + } + returns { + items: [string, ...] // list of matching strings + } + see: autocomplete, autocompleteEntities + ''' + if 'range' not in data: + data['range'] = [0, 10] + op = data.get('operator', '=') + + key = utils.get_by_id(settings.CONFIG['documentKeys'], data['key']) + order_by = key.get('autocompleteSort', False) + if order_by: + for o in order_by: + if o['operator'] != '-': + o['operator'] = '' + order_by = ['%(operator)ssort__%(key)s' % o for o in order_by] + else: + order_by = ['-items'] + + qs = parse_query({'query': data.get('query', {})}, request.user)['qs'] + response = json_response({}) + response['data']['items'] = [] + ''' + for d in qs: + value = d.json().get(data['key']) + add = False + if value: + if op == '=' and data['value'] in value: + add = True + elif op == '==' and data['value'].lower() == value.lower(): + add = True + elif op == '^' and value.lower().startswith(data['value'].lower()): + add = True + if add and value not in response['data']['items']: + response['data']['items'].append(value) + + ''' + + sort_type = key.get('sortType', key.get('type', 'string')) + qs = models.Facet.objects.filter(key=data['key']) + if data['value']: + value = unicodedata.normalize('NFKD', data['value']).lower() + if op == '=': + qs = qs.filter(value__icontains=value) + elif op == '==': + qs = qs.filter(value__iexact=value) + elif op == '^': + qs = qs.filter(value__istartswith=value) + elif op == '$': + qs = qs.filter(value__iendswith=value) + if 'query' in data: + document_query = parse_query({'query': data.get('query', {})}, request.user)['qs'] + qs = qs.filter(document__in=document_query) + qs = qs.values('value').annotate(items=Count('id')) + qs = qs.order_by(*order_by) + qs = qs[data['range'][0]:data['range'][1]] + response = json_response({}) + response['data']['items'] = [i['value'] for i in qs] + return render_to_json_response(response) +actions.register(autocompleteDocuments) diff --git a/pandora/documentcollection/__init__.py b/pandora/documentcollection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/documentcollection/managers.py b/pandora/documentcollection/managers.py new file mode 100644 index 00000000..4439546c --- /dev/null +++ b/pandora/documentcollection/managers.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +from django.db.models import Q, Manager + +from oxdjango.managers import get_operator +from oxdjango.query import QuerySet + +keymap = { + 'user': 'user__username', +} +default_key = 'name' + +def parseCondition(condition, user): + ''' + ''' + k = condition.get('key', default_key) + k = keymap.get(k, k) + if not k: + k = default_key + v = condition.get('value', '') + op = condition.get('operator') + if not op: + op = '=' + if op.startswith('!'): + op = op[1:] + exclude = True + else: + exclude = False + if k == 'id': + v = v.split(":") + if len(v) >= 2: + v = (v[0], ":".join(v[1:])) + q = Q(user__username=v[0], name=v[1]) + else: + q = Q(id__in=[]) + return q + if k == 'subscribed': + key = 'subscribed_users__username' + v = user.username + elif isinstance(v, bool): + key = k + else: + key = k + get_operator(op, 'istr') + key = str(key) + if exclude: + q = ~Q(**{key: v}) + else: + q = Q(**{key: v}) + return q + +def parseConditions(conditions, operator, user): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + +class CollectionManager(Manager): + + def get_query_set(self): + return QuerySet(self.model) + + def find(self, data, user): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + query = data.get('query', {}) + conditions = parseConditions(query.get('conditions', []), + query.get('operator', '&'), + user) + if conditions: + qs = qs.filter(conditions) + + if user.is_anonymous(): + qs = qs.filter(Q(status='public') | Q(status='featured')) + else: + qs = qs.filter(Q(status='public') | Q(status='featured') | Q(user=user)) + return qs diff --git a/pandora/documentcollection/migrations/0001_initial.py b/pandora/documentcollection/migrations/0001_initial.py new file mode 100644 index 00000000..d98f57ff --- /dev/null +++ b/pandora/documentcollection/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-08 12:34 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import documentcollection.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('document', '0003_new_fields'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Collection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('status', models.CharField(default=b'private', max_length=20)), + ('query', oxdjango.fields.DictField(default={b'static': True})), + ('type', models.CharField(default=b'static', max_length=255)), + ('description', models.TextField(default=b'')), + ('icon', models.ImageField(blank=True, default=None, upload_to=documentcollection.models.get_icon_path)), + ('view', models.TextField(default=documentcollection.models.get_collectionview)), + ('sort', oxdjango.fields.TupleField(default=documentcollection.models.get_collectionsort, editable=False)), + ('poster_frames', oxdjango.fields.TupleField(default=[], editable=False)), + ('numberofdocuments', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='CollectionDocument', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('index', models.IntegerField(default=0)), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documentcollection.Collection')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='document.Document')), + ], + ), + migrations.CreateModel( + name='Position', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('section', models.CharField(max_length=255)), + ('position', models.IntegerField(default=0)), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='position', to='documentcollection.Collection')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_positions', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='collection', + name='documents', + field=models.ManyToManyField(related_name='collections', through='documentcollection.CollectionDocument', to='document.Document'), + ), + migrations.AddField( + model_name='collection', + name='subscribed_users', + field=models.ManyToManyField(related_name='subscribed_collections', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='collection', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collections', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='position', + unique_together=set([('user', 'collection', 'section')]), + ), + migrations.AlterUniqueTogether( + name='collection', + unique_together=set([('user', 'name')]), + ), + ] diff --git a/pandora/documentcollection/migrations/__init__.py b/pandora/documentcollection/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/documentcollection/models.py b/pandora/documentcollection/models.py new file mode 100644 index 00000000..93fbe12a --- /dev/null +++ b/pandora/documentcollection/models.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division, print_function, absolute_import + +import os +import re +import subprocess +from glob import glob + +from django.db import models +from django.db.models import Max +from django.contrib.auth.models import User +from django.conf import settings +import ox + +from oxdjango.fields import DictField, TupleField + +from archive import extract + +from . import managers + + +def get_path(f, x): + return f.path(x) + +def get_icon_path(f, x): + return get_path(f, 'icon.jpg') + +def get_collectionview(): + return settings.CONFIG['user']['ui']['collectionView'] + +def get_collectionsort(): + return tuple(settings.CONFIG['user']['ui']['collectionSort']) + +class Collection(models.Model): + + class Meta: + unique_together = ("user", "name") + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + user = models.ForeignKey(User, related_name='collections') + name = models.CharField(max_length=255) + status = models.CharField(max_length=20, default='private') + _status = ['private', 'public', 'featured'] + query = DictField(default={"static": True}) + type = models.CharField(max_length=255, default='static') + description = models.TextField(default='') + + icon = models.ImageField(default=None, blank=True, upload_to=get_icon_path) + + view = models.TextField(default=get_collectionview) + sort = TupleField(default=get_collectionsort, editable=False) + + poster_frames = TupleField(default=[], editable=False) + + #is through table still required? + documents = models.ManyToManyField('document.Document', related_name='collections', + through='CollectionDocument') + + numberofdocuments = models.IntegerField(default=0) + subscribed_users = models.ManyToManyField(User, related_name='subscribed_collections') + + objects = managers.CollectionManager() + + def save(self, *args, **kwargs): + if self.query.get('static', False): + self.type = 'static' + else: + self.type = 'smart' + if self.id: + self.numberofdocuments = self.get_numberofdocuments(self.user) + super(Collection, self).save(*args, **kwargs) + + @classmethod + def get(cls, id): + id = id.split(':') + username = id[0] + collectionname = ":".join(id[1:]) + return cls.objects.get(user__username=username, name=collectionname) + + def get_documents(self, user=None): + if self.query.get('static', False): + return self.documents + from document.models import Document + return Document.objects.find({'query': self.query}, user) + + def get_numberofdocuments(self, user=None): + return self.get_documents(user).count() + + def add(self, document): + q = self.documents.filter(id=document.id) + if q.count() == 0: + l = CollectionDocument() + l.collection = self + l.document = document + l.index = CollectionDocument.objects.filter(collection=self).aggregate(Max('index'))['index__max'] + if l.index is None: + l.index = 0 + else: + l.index += 1 + l.save() + + def remove(self, document=None, documents=None): + if document: + CollectionDocument.objects.all().filter(document=document, collection=self).delete() + if documents: + CollectionDocument.objects.all().filter(document__id__in=documents, collection=self).delete() + + def __unicode__(self): + return self.get_id() + + def get_id(self): + return u'%s:%s' % (self.user.username, self.name) + + def accessible(self, user): + return self.user == user or self.status in ('public', 'featured') + + def editable(self, user): + if user.is_anonymous(): + return False + if self.user == user or \ + user.is_staff or \ + user.profile.capability('canEditFeaturedCollections') is True: + return True + return False + + def edit(self, data, user): + for key in data: + if key == 'query' and not data['query']: + setattr(self, key, {"static": True}) + elif key == 'query' and isinstance(data[key], dict): + setattr(self, key, data[key]) + elif key == 'type': + if data[key] == 'static': + self.query = {"static": True} + self.type = 'static' + else: + self.type = 'smart' + if self.query.get('static', False): + self.query = {} + elif key == 'status': + value = data[key] + if value not in self._status: + value = self._status[0] + if value == 'private': + for user in self.subscribed_users.all(): + self.subscribed_users.remove(user) + qs = Position.objects.filter(user=user, collection=self) + if qs.count() > 1: + pos = qs[0] + pos.section = 'personal' + pos.save() + elif value == 'featured': + if user.profile.capability('canEditFeaturedCollections'): + pos, created = Position.objects.get_or_create(collection=self, user=user, + section='featured') + if created: + qs = Position.objects.filter(user=user, section='featured') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + Position.objects.filter(collection=self).exclude(id=pos.id).delete() + else: + value = self.status + elif self.status == 'featured' and value == 'public': + Position.objects.filter(collection=self).delete() + pos, created = Position.objects.get_or_create(collection=self, + user=self.user, section='personal') + qs = Position.objects.filter(user=self.user, section='personal') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + for u in self.subscribed_users.all(): + pos, created = Position.objects.get_or_create(collection=self, user=u, + section='public') + qs = Position.objects.filter(user=u, section='public') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + + self.status = value + elif key == 'name': + data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + if not data['name']: + data['name'] = "Untitled" + name = data['name'] + num = 1 + while Collection.objects.filter(name=name, user=self.user).exclude(id=self.id).count() > 0: + num += 1 + name = data['name'] + ' [%d]' % num + self.name = name + elif key == 'description': + self.description = ox.sanitize_html(data['description']) + + if 'position' in data: + pos, created = Position.objects.get_or_create(collection=self, user=user) + pos.position = data['position'] + pos.section = 'featured' + if self.status == 'private': + pos.section = 'personal' + pos.save() + if 'posterFrames' in data: + self.poster_frames = tuple(data['posterFrames']) + if 'view' in data: + self.view = data['view'] + if 'sort' in data: + self.sort = tuple(data['sort']) + self.save() + if 'posterFrames' in data: + self.update_icon() + + def json(self, keys=None, user=None): + if not keys: + keys = ['id', 'name', 'user', 'type', 'query', 'status', 'subscribed', 'posterFrames', 'description', 'view'] + response = {} + for key in keys: + if key in ('items', 'documents'): + response[key] = self.get_numberofdocuments(user) + elif key == 'id': + response[key] = self.get_id() + elif key == 'user': + response[key] = self.user.username + elif key == 'query': + if not self.query.get('static', False): + response[key] = self.query + elif key == 'subscribers': + response[key] = self.subscribed_users.all().count() + elif key == 'subscribed': + if user and not user.is_anonymous(): + response[key] = self.subscribed_users.filter(id=user.id).exists() + else: + response[key] = getattr(self, { + 'posterFrames': 'poster_frames' + }.get(key, key)) + return response + + def path(self, name=''): + h = "%07d" % self.id + return os.path.join('collections', h[:2], h[2:4], h[4:6], h[6:], name) + + def update_icon(self): + frames = [] + #fixme + ''' + if not self.poster_frames: + documents = self.get_documents(self.user) + if documents.count(): + poster_frames = [] + for i in range(0, documents.count(), max(1, int(documents.count()/4))): + poster_frames.append({ + 'document': documents[int(i)].id, + 'position': documents[int(i)].poster_frame + }) + self.poster_frames = tuple(poster_frames) + self.save() + for i in self.poster_frames: + from document.models import Document + qs = Document.objects.filter(id=i['document']) + if qs.count() > 0: + if i.get('position'): + frame = qs[0].frame(i['position']) + if frame: + frames.append(frame) + ''' + self.icon.name = self.path('icon.jpg') + icon = self.icon.path + if frames: + while len(frames) < 4: + frames += frames + folder = os.path.dirname(icon) + ox.makedirs(folder) + for f in glob("%s/icon*.jpg" % folder): + os.unlink(f) + cmd = [ + settings.collection_ICON, + '-f', ','.join(frames), + '-o', icon + ] + p = subprocess.Popen(cmd, close_fds=True) + p.wait() + self.save() + + def get_icon(self, size=16): + path = self.path('icon%d.jpg' % size) + path = os.path.join(settings.MEDIA_ROOT, path) + if not os.path.exists(path): + folder = os.path.dirname(path) + ox.makedirs(folder) + if self.icon and os.path.exists(self.icon.path): + source = self.icon.path + max_size = min(self.icon.width, self.icon.height) + else: + source = os.path.join(settings.STATIC_ROOT, 'jpg/list256.jpg') + max_size = 256 + if size < max_size: + extract.resize_image(source, path, size=size) + else: + path = source + return path + +class CollectionDocument(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + collection = models.ForeignKey(Collection) + index = models.IntegerField(default=0) + document = models.ForeignKey('document.Document') + + def __unicode__(self): + return u'%s in %s' % (self.document, self.collection) + +class Position(models.Model): + + class Meta: + unique_together = ("user", "collection", "section") + + collection = models.ForeignKey(Collection, related_name='position') + user = models.ForeignKey(User, related_name='collection_positions') + section = models.CharField(max_length=255) + position = models.IntegerField(default=0) + + def __unicode__(self): + return u'%s/%s/%s' % (self.section, self.position, self.collection) + diff --git a/pandora/documentcollection/views.py b/pandora/documentcollection/views.py new file mode 100644 index 00000000..f31f50ba --- /dev/null +++ b/pandora/documentcollection/views.py @@ -0,0 +1,448 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division, print_function, absolute_import + +import os +import re +import json + +from django.db.models import Max, Sum +from django.db import transaction +from django.conf import settings +from oxdjango.decorators import login_required_json +from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response +from oxdjango.http import HttpFileResponse +import ox + +from . import models +from oxdjango.api import actions +from item import utils +from document.models import Document +from user.tasks import update_numberofcollections +from changelog.models import add_changelog + +def get_collection_or_404_json(id): + id = id.split(':') + username = id[0] + collectionname = ":".join(id[1:]) + return get_object_or_404_json(models.Collection, user__username=username, name=collectionname) + +def _order_query(qs, sort): + order_by = [] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + 'subscribed': 'subscribed_users', + 'items': 'numberofitems' + }.get(e['key'], e['key']) + order = '%s%s' % (operator, key) + order_by.append(order) + if key == 'subscribers': + qs = qs.annotate(subscribers=Sum('subscribed_users')) + if order_by: + qs = qs.order_by(*order_by, nulls_last=True) + qs = qs.distinct() + return qs + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key':'user', 'operator':'+'}, {'key':'name', 'operator':'+'}] + for key in ('keys', 'group', 'collection', 'range', 'position', 'positions', 'sort'): + if key in data: + query[key] = data[key] + query['qs'] = models.Collection.objects.find(data, user) + return query + + +def findCollections(request, data): + ''' + Finds collections for a given query + takes { + query: object, // query object, see `find` + sort: [], // collection of sort objects, see `find` + range: [int, int], // range of results to return + keys: [string] // collection of properties to return + } + returns { + items: [object] // collection of collection objects + } + notes: Possible query keys are 'featured', 'name', 'subscribed' and 'user', + possible keys are 'featured', 'name', 'query', 'subscribed' and 'user'. + see: addCollection, editCollection, find, getCollection, removeCollection, sortCollections + ''' + query = parse_query(data, request.user) + + #order + is_section_request = query['sort'] == [{u'operator': u'+', u'key': u'position'}] + + def is_featured_condition(x): + return x['key'] == 'status' and \ + x['value'] == 'featured' and \ + x['operator'] in ('=', '==') + + is_featured = any( + is_featured_condition(x) + for x in data.get('query', {}).get('conditions', []) + ) + + if is_section_request: + qs = query['qs'] + if not is_featured and not request.user.is_anonymous(): + qs = qs.filter(position__in=models.Position.objects.filter(user=request.user)) + qs = qs.order_by('position__position') + else: + qs = _order_query(query['qs'], query['sort']) + + response = json_response() + if 'keys' in data: + qs = qs[query['range'][0]:query['range'][1]] + + response['data']['items'] = [l.json(data['keys'], request.user) for l in qs] + elif 'position' in data: + #FIXME: actually implement position requests + response['data']['position'] = 0 + elif 'positions' in data: + ids = [i.get_id() for i in qs] + response['data']['positions'] = utils.get_positions(ids, query['positions']) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findCollections) + +def getCollection(request, data): + ''' + Gets a collection by id + takes { + id: string // collection id + } + returns { + id: string, // collection id + section: string, // collections section (like 'personal') + ... // more key/value pairs + } + see: addCollection, editCollection, findCollections, removeCollection, sortCollections + ''' + if 'id' in data: + response = json_response() + collection = get_collection_or_404_json(data['id']) + if collection.accessible(request.user): + response['data'] = collection.json(user=request.user) + else: + response = json_response(status=403, text='not allowed') + else: + response = json_response(status=404, text='not found') + return render_to_json_response(response) +actions.register(getCollection) + +@login_required_json +def addCollectionItems(request, data): + ''' + Adds one or more items to a static collection + takes { + collection: string, // collection id + items: [string], // either collection of item ids + query: object // or query object, see `find` + } + returns {} + see: find, orderCollectionItems, removeCollectionItems + ''' + collection = get_collection_or_404_json(data['collection']) + if 'items' in data: + if collection.editable(request.user): + with transaction.atomic(): + items = [ox.fromAZ(id) for id in data['items']] + for item in Document.objects.filter(id__in=items): + collection.add(item) + response = json_response(status=200, text='items added') + add_changelog(request, data, data['collection']) + else: + response = json_response(status=403, text='not allowed') + elif 'query' in data: + response = json_response(status=501, text='not implemented') + else: + response = json_response(status=501, text='not implemented') + return render_to_json_response(response) +actions.register(addCollectionItems, cache=False) + + +@login_required_json +def removeCollectionItems(request, data): + ''' + Removes one or more items from a static collection + takes { + collection: string, // collection id + items: [itemId], // either collection of item ids + query: object // or query object, see `find` + } + returns {} + see: addCollectionItems, find, orderCollectionItems + ''' + collection = get_collection_or_404_json(data['collection']) + if 'items' in data: + if collection.editable(request.user): + items = [ox.fromAZ(id) for id in data['items']] + collection.remove(documents=items) + response = json_response(status=200, text='items removed') + add_changelog(request, data, data['collection']) + else: + response = json_response(status=403, text='not allowed') + elif 'query' in data: + response = json_response(status=501, text='not implemented') + + else: + response = json_response(status=501, text='not implemented') + return render_to_json_response(response) +actions.register(removeCollectionItems, cache=False) + +@login_required_json +def orderCollectionItems(request, data): + ''' + Sets the manual ordering of items in a given collection + takes { + collection: string, // collection id + ids: [string] // ordered collection of item ids + } + returns {} + notes: There is no UI for this yet. + see: addCollectionItems, removeCollectionItems + ''' + collection = get_collection_or_404_json(data['collection']) + response = json_response() + if collection.editable(request.user) and collection.type == 'static': + index = 0 + with transaction.atomic(): + for i in data['ids']: + models.CollectionItem.objects.filter(collection=collection, item__public_id=i).update(index=index) + index += 1 + else: + response = json_response(status=403, text='permission denied') + return render_to_json_response(response) +actions.register(orderCollectionItems, cache=False) + + +@login_required_json +def addCollection(request, data): + ''' + Adds a new collection + takes { + name: value, // collection name (optional) + ... // more key/value pairs + } + returns { + id: string, // collection id + name: string, // collection name + ... // more key/value pairs + } + notes: Possible keys are 'description', 'items', 'name', 'query', 'sort', + 'type' and 'view'. + see: editCollection, findCollections, getCollection, removeCollection, sortCollections + ''' + data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + name = data['name'] + if not name: + name = "Untitled" + num = 1 + created = False + while not created: + collection, created = models.Collection.objects.get_or_create(name=name, user=request.user) + num += 1 + name = data['name'] + ' [%d]' % num + + del data['name'] + if data: + collection.edit(data, request.user) + else: + collection.save() + update_numberofcollections.delay(request.user.username) + + if 'items' in data: + items = [ox.fromAZ(id) for id in data['items']] + for item in Document.objects.filter(id__in=items): + collection.add(item) + + if collection.status == 'featured': + pos, created = models.Position.objects.get_or_create(collection=collection, + user=request.user, section='featured') + qs = models.Position.objects.filter(section='featured') + else: + pos, created = models.Position.objects.get_or_create(collection=collection, + user=request.user, section='personal') + qs = models.Position.objects.filter(user=request.user, section='personal') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + response = json_response(status=200, text='created') + response['data'] = collection.json() + add_changelog(request, data, collection.get_id()) + return render_to_json_response(response) +actions.register(addCollection, cache=False) + + +@login_required_json +def editCollection(request, data): + ''' + Edits a collection + takes { + id: string, // collection id + key: value, // property id and new value + ... // more key/value pairs + } + returns { + id: string, // collection id + ... // more key/value pairs + } + notes: Possible keys are 'name', 'position', 'posterFrames', 'query' and + 'status'. 'posterFrames' is an array of {item, position}. If you change + 'status', you have to pass 'position' (the position of the collection in its new + collection folder). + see: addCollection, findCollections, getCollection, removeCollection, sortCollections + ''' + collection = get_collection_or_404_json(data['id']) + if collection.editable(request.user): + response = json_response() + collection.edit(data, request.user) + response['data'] = collection.json(user=request.user) + add_changelog(request, data) + else: + response = json_response(status=403, text='not allowed') + return render_to_json_response(response) +actions.register(editCollection, cache=False) + +@login_required_json +def removeCollection(request, data): + ''' + Removes a collection + takes { + id: string // collection id + } + returns {} + see: addCollection, editCollection, findCollections, getCollection, sortCollections + ''' + collection = get_collection_or_404_json(data['id']) + response = json_response() + if collection.editable(request.user): + add_changelog(request, data) + collection.delete() + update_numberofcollections.delay(request.user.username) + else: + response = json_response(status=403, text='not allowed') + return render_to_json_response(response) +actions.register(removeCollection, cache=False) + + +@login_required_json +def subscribeToCollection(request, data): + ''' + Adds a collection to favorites + takes { + id: string, // collection id + user: string // username (admin-only) + } + returns {} + see: unsubscribeFromCollection + ''' + collection = get_collection_or_404_json(data['id']) + user = request.user + if collection.status == 'public' and \ + collection.subscribed_users.filter(username=user.username).count() == 0: + collection.subscribed_users.add(user) + pos, created = models.Position.objects.get_or_create(collection=collection, user=user, section='public') + if created: + qs = models.Position.objects.filter(user=user, section='public') + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + add_changelog(request, data) + response = json_response() + return render_to_json_response(response) +actions.register(subscribeToCollection, cache=False) + + +@login_required_json +def unsubscribeFromCollection(request, data): + ''' + Removes a collection from favorites + takes { + id: string, // collection id + user: string // username (admin-only) + } + returns {} + see: subscribeToCollection + ''' + collection = get_collection_or_404_json(data['id']) + user = request.user + collection.subscribed_users.remove(user) + models.Position.objects.filter(collection=collection, user=user, section='public').delete() + response = json_response() + add_changelog(request, data) + return render_to_json_response(response) +actions.register(unsubscribeFromCollection, cache=False) + + +@login_required_json +def sortCollections(request, data): + ''' + Sets the order of collections in a given section + takes { + section: string, // collections section + ids: [string] // ordered collection of collections + } + returns {} + notes: Possible sections are 'personal', 'favorite' and 'featured'. Setting + the order of featured collections requires the appropriate capability. + see: addCollection, editCollection, findCollections, getCollection, removeCollection + ''' + position = 0 + section = data['section'] + section = { + 'favorite': 'public' + }.get(section, section) + #ids = collection(set(data['ids'])) + ids = data['ids'] + if section == 'featured' and not request.user.profile.capability('canEditFeaturedCollections'): + response = json_response(status=403, text='not allowed') + else: + user = request.user + if section == 'featured': + for i in ids: + l = get_collection_or_404_json(i) + qs = models.Position.objects.filter(section=section, collection=l) + if qs.count() > 0: + pos = qs[0] + else: + pos = models.Position(collection=l, user=user, section=section) + if pos.position != position: + pos.position = position + pos.save() + position += 1 + models.Position.objects.filter(section=section, collection=l).exclude(id=pos.id).delete() + else: + for i in ids: + l = get_collection_or_404_json(i) + pos, created = models.Position.objects.get_or_create(collection=l, + user=request.user, section=section) + if pos.position != position: + pos.position = position + pos.save() + position += 1 + + response = json_response() + return render_to_json_response(response) +actions.register(sortCollections, cache=False) + + +def icon(request, id, size=16): + if not size: + size = 16 + + id = id.split(':') + username = id[0] + collectionname = ":".join(id[1:]) + qs = models.Collection.objects.filter(user__username=username, name=collectionname) + if qs.count() == 1 and qs[0].accessible(request.user): + collection = qs[0] + icon = collection.get_icon(int(size)) + else: + icon = os.path.join(settings.STATIC_ROOT, 'jpg/list256.jpg') + return HttpFileResponse(icon, content_type='image/jpeg') diff --git a/pandora/entity/views.py b/pandora/entity/views.py index 8581d122..035ce621 100644 --- a/pandora/entity/views.py +++ b/pandora/entity/views.py @@ -144,7 +144,7 @@ def autocompleteEntities(request, data): values = leading_matches + leading_word_matches + matches else: values = [e.name for e in qs] - values = values[data['range'][0]:data['range'][1]] + values = values[data['range'][0]:data['range'][1]] response = json_response({}) response['data']['items'] = values return render_to_json_response(response) diff --git a/pandora/item/management/commands/sqlfindindex.py b/pandora/item/management/commands/sqlfindindex.py index 3823a83b..8b2fa88d 100644 --- a/pandora/item/management/commands/sqlfindindex.py +++ b/pandora/item/management/commands/sqlfindindex.py @@ -34,11 +34,13 @@ class Command(BaseCommand): if settings.DB_GIN_TRGM: import entity.models + import document.models for table, column in ( (models.ItemFind._meta.db_table, 'value'), # Item Find (models.Clip._meta.db_table, 'findvalue'), # Clip Find (models.Annotation._meta.db_table, 'findvalue'), # Annotation Find (entity.models.Find._meta.db_table, 'value'), # Entity Find + (document.models.Find._meta.db_table, 'value'), # Document Find ): cursor = connection.cursor() indexes = connection.introspection.get_indexes(cursor, table) diff --git a/pandora/item/management/commands/sync_itemsort.py b/pandora/item/management/commands/sync_itemsort.py index b7656ade..2f7b7a2e 100644 --- a/pandora/item/management/commands/sync_itemsort.py +++ b/pandora/item/management/commands/sync_itemsort.py @@ -52,7 +52,7 @@ class Command(BaseCommand): changes.append(sql) rebuild = True elif f.__class__.__name__ != db_types[name]: - sql = 'ALTER TABLE "%s" DROP COLUMN "%s"' % (table_name, name ) + sql = 'ALTER TABLE "%s" DROP COLUMN "%s"' % (table_name, name) changes.append(sql) sql = 'ALTER TABLE "%s" ADD COLUMN "%s" %s' % (table_name, name, col_type) changes.append(sql) @@ -116,3 +116,6 @@ class Command(BaseCommand): if options['debug']: print(i) i.update_sort() + # and udpate doucments + import document.sync_sort + document.sync_sort.update_tables(options['debug']) diff --git a/pandora/item/models.py b/pandora/item/models.py index a95c1625..7ef99870 100644 --- a/pandora/item/models.py +++ b/pandora/item/models.py @@ -24,6 +24,7 @@ from django.utils import datetime_safe import ox from oxdjango import fields +from oxdjango.sortmodel import get_sort_field import ox.web.imdb import ox.image @@ -1741,27 +1742,9 @@ for key in filter(lambda k: k.get('sort', False) or k['type'] in ('integer', 'ti sort_type = key.get('sortType', key['type']) if isinstance(sort_type, list): sort_type = sort_type[0] - model = { - 'char': (models.CharField, dict(null=True, max_length=1000, db_index=True)), - 'year': (models.CharField, dict(null=True, max_length=4, db_index=True)), - 'integer': (models.BigIntegerField, dict(null=True, blank=True, db_index=True)), - 'float': (models.FloatField, dict(null=True, blank=True, db_index=True)), - 'date': (models.DateTimeField, dict(null=True, blank=True, db_index=True)) - }[{ - 'layer': 'char', - 'string': 'char', - 'title': 'char', - 'person': 'char', - 'year': 'year', - 'words': 'integer', - 'length': 'integer', - 'date': 'date', - 'hue': 'float', - 'time': 'integer', - 'enum': 'integer', - }.get(sort_type, sort_type)] + field = get_sort_field(sort_type) if name not in attrs: - attrs[name] = model[0](**model[1]) + attrs[name] = field[0](**field[1]) ItemSort = type('ItemSort', (models.Model,), attrs) ItemSort.fields = [f.name for f in ItemSort._meta.fields] diff --git a/pandora/item/views.py b/pandora/item/views.py index d075015e..bde8e3a5 100644 --- a/pandora/item/views.py +++ b/pandora/item/views.py @@ -326,7 +326,7 @@ def autocomplete(request, data): returns { items: [string, ...] // list of matching strings } - see: autocompleteEntities + see: autocompleteDocuments, autocompleteEntities ''' if 'range' not in data: data['range'] = [0, 10] diff --git a/pandora/oxdjango/sortmodel.py b/pandora/oxdjango/sortmodel.py new file mode 100644 index 00000000..de4d10e5 --- /dev/null +++ b/pandora/oxdjango/sortmodel.py @@ -0,0 +1,25 @@ +from django.db import models + +FIELDS = { + 'char': (models.CharField, dict(null=True, max_length=1000, db_index=True)), + 'year': (models.CharField, dict(null=True, max_length=4, db_index=True)), + 'integer': (models.BigIntegerField, dict(null=True, blank=True, db_index=True)), + 'float': (models.FloatField, dict(null=True, blank=True, db_index=True)), + 'date': (models.DateTimeField, dict(null=True, blank=True, db_index=True)) +} + +def get_sort_field(sort_type): + return FIELDS[{ + 'layer': 'char', + 'string': 'char', + 'text': 'char', + 'title': 'char', + 'person': 'char', + 'year': 'year', + 'words': 'integer', + 'length': 'integer', + 'date': 'date', + 'hue': 'float', + 'time': 'integer', + 'enum': 'integer', + }.get(sort_type, sort_type)] diff --git a/pandora/settings.py b/pandora/settings.py index de494bc2..46e051bc 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -138,6 +138,7 @@ INSTALLED_APPS = ( 'user', 'urlalias', 'tv', + 'documentcollection', 'document', 'entity', 'websocket', diff --git a/pandora/urls.py b/pandora/urls.py index 80d03b14..fd50c8cf 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -19,6 +19,7 @@ import oxdjango.api.urls import app.views import archive.views import document.views +import documentcollection.views import text.views import user.views import edit.views @@ -42,6 +43,7 @@ urlpatterns = [ url(r'^file/(?P.*)$', archive.views.lookup_file), url(r'^api/?', include(oxdjango.api.urls)), url(r'^resetUI$', user.views.reset_ui), + url(r'^collection/(?P.*?)/icon(?P\d*).jpg$', documentcollection.views.icon), url(r'^documents/(?P[A-Z0-9]+)/(?P\d*)p(?P[\d,]*).jpg$', document.views.thumbnail), url(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3})$', document.views.file), url(r'^edit/(?P.*?)/icon(?P\d*).jpg$', edit.views.icon), diff --git a/pandora/user/migrations/0003_sessiondata_numberofcollections.py b/pandora/user/migrations/0003_sessiondata_numberofcollections.py new file mode 100644 index 00000000..01d3d293 --- /dev/null +++ b/pandora/user/migrations/0003_sessiondata_numberofcollections.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-08 12:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_auto_20160219_1734'), + ] + + operations = [ + migrations.AddField( + model_name='sessiondata', + name='numberofcollections', + field=models.IntegerField(default=0, null=True), + ), + ] diff --git a/pandora/user/models.py b/pandora/user/models.py index b183ee43..dbbb55c2 100644 --- a/pandora/user/models.py +++ b/pandora/user/models.py @@ -18,6 +18,7 @@ from ox.utils import json from itemlist.models import List, Position import text import edit +import documentcollection.models from . import managers from . import tasks @@ -44,6 +45,7 @@ class SessionData(models.Model): browser = models.CharField(max_length=255, null=True) numberoflists = models.IntegerField(default=0, null=True) + numberofcollections = models.IntegerField(default=0, null=True) objects = managers.SessionDataManager() @@ -96,6 +98,7 @@ class SessionData(models.Model): else: self.groupssort = None self.numberoflists = self.user.lists.count() + self.numberofcollections = self.user.collections.count() else: self.groupssort = None super(SessionData, self).save(*args, **kwargs) @@ -157,6 +160,7 @@ class SessionData(models.Model): 'newsletter': False, 'notes': '', 'numberoflists': 0, + 'numberofcollections': 0, 'screensize': self.screensize, 'system': ua['system']['string'], 'timesseen': self.timesseen, @@ -173,6 +177,7 @@ class SessionData(models.Model): j['newsletter'] = p.newsletter j['notes'] = p.notes j['numberoflists'] = self.numberoflists + j['numberofcollections'] = self.numberofcollections if keys: for key in list(j): if key not in keys: @@ -249,9 +254,12 @@ def get_ui(user_ui, user=None): ui[key] = new[key] return ui ui = update_ui(ui, user_ui) - if not 'lists' in ui: + if 'lists' not in ui: ui['lists'] = {} + if 'collections' not in ui: + ui['collections'] = {} + def add(lists, section): ids = [] for l in lists: @@ -278,7 +286,30 @@ def get_ui(user_ui, user=None): ''' ids.append(id) return ids - + + def add_collections(collections, section): + Position = documentcollection.models.Position + ids = [] + for l in collections: + qs = Position.objects.filter(section=section) + if section == 'featured': + try: + pos = Position.objects.get(collection=l, section=section) + created = False + except Position.DoesNotExist: + pos = Position(collection=l, section=section, user=l.user) + pos.save() + created = True + else: + pos, created = Position.objects.get_or_create(collection=l, user=user, section=section) + qs = qs.filter(user=user) + if created: + pos.position = qs.aggregate(Max('position'))['position__max'] + 1 + pos.save() + id = l.get_id() + ids.append(id) + return ids + def add_texts(texts, section): P = text.models.Position ids = [] @@ -323,6 +354,7 @@ def get_ui(user_ui, user=None): ids.append(t.get_id()) return ids + # lists (items) ids = [''] if user: ids += add(user.lists.exclude(status="featured"), 'personal') @@ -331,11 +363,26 @@ def get_ui(user_ui, user=None): for i in list(ui['lists']): if i not in ids: del ui['lists'][i] + + # collections (documents) + ids = [''] + if user: + ids += add_collections(user.collections.exclude(status="featured"), 'personal') + ids += add_collections(user.subscribed_collections.filter(status='public'), 'public') + ids += add_collections(documentcollection.models.Collection.objects.filter(status='featured'), 'featured') + for i in list(ui['collections']): + if i not in ids: + del ui['collections'][i] + + # texts (remove) tids = [''] if user: tids += add_texts(user.texts.exclude(status="featured"), 'personal') tids += add_texts(user.subscribed_texts.filter(status='public'), 'public') tids += add_texts(text.models.Text.objects.filter(status='featured'), 'featured') + + # edits + tids = [''] if user: tids += add_edits(user.edits.exclude(status="featured"), 'personal') tids += add_edits(user.subscribed_edits.filter(status='public'), 'public') diff --git a/pandora/user/tasks.py b/pandora/user/tasks.py index ae2ad567..92b60363 100644 --- a/pandora/user/tasks.py +++ b/pandora/user/tasks.py @@ -46,3 +46,13 @@ def update_numberoflists(username): ).update( numberoflists=user.lists.count() ) + +@task(ignore_results=True, queue='default') +def update_numberofcollections(username): + from . import models + user = models.User.objects.get(username=username) + models.SessionData.objects.filter( + user=user + ).update( + numberofcollections=user.collections.count() + ) diff --git a/pandora/user/views.py b/pandora/user/views.py index bc79b347..81cb94ee 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -800,7 +800,10 @@ def setUI(request, data): access, created = Access.objects.get_or_create(item=item, user=None) if not created: access.save() - + if data.get('document'): + import document.models + doc = get_object_or_404_json(document.models.Document, id=ox.fromAZ(data['document'])) + doc.update_access(request.user) response = json_response() return render_to_json_response(response) actions.register(setUI, cache=False) diff --git a/static/js/UI.js b/static/js/UI.js index e5811ba9..d4294283 100644 --- a/static/js/UI.js +++ b/static/js/UI.js @@ -28,6 +28,12 @@ pandora.UI = (function() { pandora.user.ui._findState = pandora.getFindState( pandora.user.ui.find ); + pandora.user.ui._collection = pandora.getCollectionState( + pandora.user.ui.findDocuments + ); + pandora.user.ui._findDocumentsState = pandora.getFindDocumentsState( + pandora.user.ui.findDocuments + ); Ox.Theme(pandora.user.ui.theme); pandora.$ui.appPanel.reload(); }); @@ -40,6 +46,9 @@ pandora.UI = (function() { var add = {}, args, + collection, + collectionView, + collectionSettings = pandora.site.collectionSettings, editSettings = pandora.site.editSettings, item, list, @@ -69,6 +78,76 @@ pandora.UI = (function() { } else if (args.section == 'edits') { trigger.section = args.section; trigger.edit = args.edit; + } else if (pandora.user.ui.section == 'documents' || args.section == 'documents') { + if ('findDocuments' in args) { + // the challenge here is that find may change list, + // and list may then change listSort and listView, + // which we don't want to trigger, since find triggers + // (values we put in add will be changed, but won't trigger) + collection = pandora.getCollectionState(args.findDocuments); + pandora.user.ui._collection = collection; + pandora.user.ui._findDocumentsState = pandora.getFindDocumentsState(args.findDocuments); + if (pandora.$ui.appPanel && !pandora.stayInItemView) { + // if we're not on page load, and if find isn't a context change + // caused by an edit, then switch from item view to list view + args['document'] = ''; + } + if (collection != self.previousUI._collection) { + Ox.Log('UI', 'FIND HAS CHANGED COLLECTION', self.previousUI._collection, '>', collection); + // if find has changed collection + Ox.forEach(collectionSettings, function(collectionSetting, setting) { + // then for each setting that corresponds to a collection setting + if (!Ox.isUndefined(args[setting])) { + add[setting] = args[setting]; + } else if ( + !pandora.user.ui.collections[collection] + || Ox.isUndefined(pandora.user.ui.collections[collection][collectionSetting]) + ) { + // either add the default setting + add[setting] = pandora.site.user.ui[setting]; + } else { + // or the existing collection setting + add[setting] = pandora.user.ui.collections[collection][collectionSetting]; + } + }); + } + } else { + collection = self.previousUI._collection; + } + if (!pandora.user.ui.collections[collection]) { + add['collections.' + that.encode(collection)] = {}; + } + Ox.forEach(collectionSettings, function(collectionSetting, setting) { + // for each setting that corresponds to a collection setting + // set that collection setting to + var key = 'collections.' + that.encode(collection) + '.' + collectionSetting; + if (setting in args) { + // the setting passed to UI.set + add[key] = args[setting]; + } else if (setting in add) { + // or the setting changed via find + add[key] = add[setting]; + } else if (!pandora.user.ui.collections[collection]) { + // or the default setting + add[key] = pandora.site.user.ui[setting]; + } + }); + // set nested lisColumnWidth updates + Ox.forEach(args, function(value, key) { + if (Ox.startsWith(key, 'collectionColumnWidth.')) { + key = 'collections.' + that.encode(collection) + '.columnWidth.' + + key.slice('collectionColumnWidth.'.length); + if (!(key in add)) { + add[key] = value; + } + } + }); + + if (args.document) { + // when switching to an item, update list selection + add['collectionSelection'] = [args.document]; + add['collections.' + that.encode(collection) + '.selection'] = [args.document]; + } } else { if ('find' in args) { // the challenge here is that find may change list, @@ -296,7 +375,7 @@ pandora.UI = (function() { } }); }); - + pandora.URL.update(Object.keys( !pandora.$ui.appPanel ? args : trigger )); diff --git a/static/js/URL.js b/static/js/URL.js index 86bbac30..316798f7 100644 --- a/static/js/URL.js +++ b/static/js/URL.js @@ -20,13 +20,15 @@ pandora.URL = (function() { Ox.contains(Object.keys(pandora.site.user.ui.part), state.page) ) { state.part = pandora.user.ui.part[state.page]; + /* if ( state.page == 'documents' && pandora.user.ui.documents[state.part] - && pandora.user.ui.documents[state.part].position + && pandora.user.ui.documents[state.part].position ) { state.span = pandora.user.ui.documents[state.part].position; } + */ } } else { @@ -65,6 +67,25 @@ pandora.URL = (function() { : [] ); } + } else if (pandora.user.ui.section == 'documents') { + if (!pandora.user.ui.document) { + state.view = pandora.user.ui.collectionView; + state.sort = [pandora.user.ui.collectionSort[0]]; + state.find = pandora.user.ui.findDocuments; + } else { + var documentState = pandora.user.ui.documents[state.item] || {}, + position = documentState.position || 0; + if (pandora.user.ui.documentView == 'view') { + if (documentState.name) { + state.span = documentState.name; + } else if (position) { + state.span = [position]; + } + } else { + state.view = pandora.user.ui.documentView; + } + state.sort = [pandora.user.ui.collectionSort[0]]; + } } else if (pandora.user.ui.section == 'edits') { var editPoints = pandora.user.ui.edits[state.item] || {}; if (state.item) { @@ -118,10 +139,15 @@ pandora.URL = (function() { var set = {}; Ox.Log('URL', 'setState:', state); + if (state.type == 'texts') { + state.type = 'documents'; + } pandora.user.ui._list = pandora.getListState(pandora.user.ui.find); pandora.user.ui._filterState = pandora.getFilterState(pandora.user.ui.find); pandora.user.ui._findState = pandora.getFindState(pandora.user.ui.find); + pandora.user.ui._collection = pandora.getCollectionState(pandora.user.ui.findDocuments); + pandora.user.ui._findDocumentsState = pandora.getFindDocumentsState(pandora.user.ui.findDocuments); if (Ox.isEmpty(state)) { @@ -148,9 +174,6 @@ pandora.URL = (function() { ) && state.part) { set['part.' + state.page] = state.part; } - if (state.span) { - set['documents.' + state.part] = {position: state.span}; - } pandora.UI.set(set); callback && callback(); @@ -222,7 +245,16 @@ pandora.URL = (function() { set.find = pandora.site.user.ui.find; } } - + } else if (state.type == 'documents') { + if (state.view) { + set[!state.item ? 'collectionView' : 'documentView'] = state.view; + } + if (state.span) { + set['documents.' + state.item] = {position: state.span}; + } + if (!state.item && state.find) { + set.findDocuments = state.find; + } } else if (state.type == 'edits') { if (state.view) { @@ -248,16 +280,6 @@ pandora.URL = (function() { set[key + '.clip'] = state.span; } } - - } else if (state.type == 'texts') { - - if (state.span) { - set['texts.' + pandora.UI.encode(state.item)] = { - position: Ox.isArray(state.span) ? state.span[0] : 0, - name: Ox.isArray(state.span) ? '' : state.span - }; - } - } Ox.Request.cancel(); @@ -368,6 +390,19 @@ pandora.URL = (function() { .concat(pandora.site.itemKeys); }); + // Documents + views['documents'] = { + list: ['grid', 'list'], + item: ['view', 'info'] + }; + sortKeys['documents'] = { + list: { + list: pandora.site.documentKeys, + grid: pandora.site.documentKeys + }, + item: {} + }; + // Texts views['texts'] = { list: [], @@ -377,7 +412,6 @@ pandora.URL = (function() { list: {}, item: {} }; - return { views: views, sortKeys: sortKeys @@ -388,7 +422,7 @@ pandora.URL = (function() { that.init = function() { var itemsSection = pandora.site.itemsSection, - findKeys, spanType = {}; + findKeys = {}, spanType = {}; spanType[itemsSection] = { list: { @@ -411,12 +445,25 @@ pandora.URL = (function() { annotations: 'duration' } }; + spanType['documents'] = { + list: {}, + item: { + view: 'string', + } + }; spanType['texts'] = { list: {}, item: {text: 'string'} }; - findKeys = [{id: 'list', type: 'string'}].concat(pandora.site.itemKeys); + findKeys[itemsSection] = [ + {id: 'list', type: 'string'}, + ].concat(pandora.site.itemKeys); + + findKeys['edits'] = []; + findKeys['documents'] = [ + {id: 'collection', type: 'string'} + ].concat(pandora.site.documentKeys); self.URL = Ox.URL(Ox.extend({ findKeys: findKeys, @@ -426,14 +473,14 @@ pandora.URL = (function() { getSort: pandora.getSort, getSpan: pandora.getSpan, pages: [].concat( - ['home', 'software', 'api', 'help', 'tv', 'documents', 'entities'], + ['home', 'software', 'api', 'help', 'tv', 'entities'], pandora.site.sitePages.map(function(page) { return page.id; }), ['preferences', 'signup', 'signin', 'signout'] ), spanType: spanType, - types: [pandora.site.itemName.plural.toLowerCase(), 'edits', 'texts'], + types: [pandora.site.itemName.plural.toLowerCase(), 'edits', 'documents', 'texts'], }, getOptions())); window.addEventListener('hashchange', function() { @@ -508,7 +555,7 @@ pandora.URL = (function() { that.push = function(stateOrURL, expandURL) { var state, title = pandora.getPageTitle(stateOrURL) - || pandora.getDocumentTitle(), + || pandora.getWindowTitle(), url; pandora.replaceURL = expandURL; if (Ox.isObject(stateOrURL)) { @@ -524,7 +571,7 @@ pandora.URL = (function() { that.replace = function(stateOrURL, title) { var state, title = pandora.getPageTitle(stateOrURL) - || pandora.getDocumentTitle(), + || pandora.getWindowTitle(), url; if (Ox.isObject(stateOrURL)) { state = stateOrURL; @@ -576,7 +623,7 @@ pandora.URL = (function() { state = getState(); self.URL[action]( state, - pandora.getPageTitle(state) || pandora.getDocumentTitle() + pandora.getPageTitle(state) || pandora.getWindowTitle() ); pandora.replaceURL = false; } diff --git a/static/js/addDocumentDialog.js b/static/js/addDocumentDialog.js new file mode 100644 index 00000000..2ff25ee0 --- /dev/null +++ b/static/js/addDocumentDialog.js @@ -0,0 +1,109 @@ +'use strict'; + +pandora.ui.addDocumentDialog = function(options) { + options = options || {}; + + var input = ''; + + var selected = options.selected ? options.selected : 'upload'; + + var $button; + + var $panel = Ox.TabPanel({ + content: function(id) { + var $content = Ox.Element().css({padding: '8px'}); + var $input = Ox.Input({ + changeOnKeypress: true, + disabled: id == 'upload', + label: Ox._(id == 'add' ? 'Title' : id == 'upload' ? 'File': 'URL'), + labelWidth: 64, + placeholder: '', + width: 512 + }).css({ + margin: '8px' + }).bindEvent({ + change: function(data) { + $button.options({disabled: !data.value}); + input = data.value; + } + }).appendTo($content); + return $content; + }, + tabs: [ + { + id: 'add', + title: Ox._('Add {0}', [Ox._('Document')]), + disabled: false, + selected: selected == 'add' + }, + { + id: 'upload', + title: Ox._('Upload Documents'), + selected: selected == 'upload' + } + ] + }).bindEvent({ + change: function(data) { + selected = data.selected; + that.options({buttons: [createButton()]}); + } + }); + + var $screen = Ox.LoadingScreen({ + size: 16 + }); + + var that = Ox.Dialog({ + buttons: [createButton()], + closeButton: true, + content: $panel, + height: 72, + removeOnClose: true, + title: Ox._('Add {0}', [Ox._('Document')]), + width: 544 + }); + + function createButton() { + $button = Ox[selected == 'upload' ? 'FileButton' : 'Button']({ + disabled: selected != 'upload', + id: selected, + title: selected == 'add' + ? Ox._('Add {0}', ['Document']) + : Ox._('Select Documents'), + width: selected == 'add' ? 192 : 128 + }).bindEvent({ + click: function(data) { + if (selected == 'add') { + that.options({content: $screen.start()}); + $button.options({disabled: true}); + pandora.api.addDocument({title: input}, function(result) { + Ox.Request.clearCache('find'); + $screen.stop(); + that.close(); + pandora.UI.set({ + document: result.data.id + }); + }); + } else if (selected == 'upload' && data.files.length > 0) { + that.close(); + pandora.ui.uploadDocumentDialog({ + files: data.files + }, function(files) { + if (files) { + Ox.Request.clearCache('findDocuments'); + if (pandora.user.ui.document) { + pandora.UI.set({document: ''}); + } else { + pandora.$ui.list && pandora.$ui.list.reloadList(); + } + } + }).open(); + } + } + }); + return $button; + } + + return that; + +}; diff --git a/static/js/allItems.js b/static/js/allItems.js index 68e275e5..516321bb 100644 --- a/static/js/allItems.js +++ b/static/js/allItems.js @@ -8,8 +8,10 @@ pandora.ui.allItems = function(section) { var canAddItems = !pandora.site.itemRequiresVideo && pandora.site.capabilities.canAddItems[pandora.user.level], canUploadVideo = pandora.site.capabilities.canAddItems[pandora.user.level], + canAddDocuments = pandora.site.capabilities.canAddDocuments[pandora.user.level], + isSelected = pandora.user.ui._list || pandora.user.ui._collection, that = Ox.Element() - .addClass('OxSelectableElement' + (pandora.user.ui._list ? '' : ' OxSelected')) + .addClass('OxSelectableElement' + (isSelected ? '' : ' OxSelected')) .css({ height: '16px', cursor: 'default', @@ -19,13 +21,21 @@ pandora.ui.allItems = function(section) { click: function() { that.gainFocus(); if (section == 'items') { - pandora.user.ui._list && pandora.UI.set({find: {conditions: [], operator: '&'}}); + pandora.user.ui._list && pandora.UI.set({ + find: {conditions: [], operator: '&'} + }); + } else if (section == 'documents') { + pandora.user.ui._collection && pandora.UI.set({ + findDocuments: {conditions: [], operator: '&'} + }); } else { pandora.UI.set(section.slice(0, -1), ''); } } }) .bindEvent({ + pandora_document: updateSelected, + pandora_finddocuments: updateSelected, pandora_edit: updateSelected, pandora_find: updateSelected, pandora_section: updateSelected, @@ -95,6 +105,56 @@ pandora.ui.allItems = function(section) { }, function(result) { that.update(result.data.items); }); + } else if (section == 'documents') { + $items = $('
') + .css({ + float: 'left', + width: '42px', + margin: '1px 4px 1px 3px', + textAlign: 'right' + }) + .appendTo(that); + $buttons[0] = Ox.Button({ + style: 'symbol', + title: 'add', + tooltip: canAddDocuments ? Ox._('Add {0}', [Ox._('Document')]) : '', + type: 'image' + }) + .css({opacity: canAddDocuments ? 1 : 0.25}) + .hide() + .bindEvent({ + click: function(data) { + if (canAddDocuments) { + pandora.$ui.addDocumentDialog = pandora.ui.addDocumentDialog({ + selected: 'add' + }).open(); + } + } + }) + .appendTo(that); + $buttons[1] = Ox.Button({ + style: 'symbol', + title: 'upload', + tooltip: canAddDocuments ? Ox._('Upload {0}', [Ox._('Document')]) : '', + type: 'image' + }) + .css({opacity: canAddDocuments ? 1 : 0.25}) + .hide() + .bindEvent({ + click: function() { + if (canAddDocuments) { + pandora.$ui.addDocumentDialog = pandora.ui.addDocumentDialog({ + selected: 'upload' + }).open(); + } + } + }) + .appendTo(that); + pandora.api.findDocuments({ + query: {conditions: [], operator: '&'} + }, function(result) { + that.update(result.data.items); + }); } else if (section == 'texts') { $buttons[0] = Ox.Button({ style: 'symbol', @@ -124,6 +184,7 @@ pandora.ui.allItems = function(section) { function updateSelected() { that[ (section == 'items' && pandora.user.ui._list) + || (section == 'documents' && pandora.user.ui._collection) || (section == 'edits' && pandora.user.ui.edit) || (section == 'texts' && pandora.user.ui.text) ? 'removeClass' : 'addClass' diff --git a/static/js/backButton.js b/static/js/backButton.js index 868c0624..b98781b8 100644 --- a/static/js/backButton.js +++ b/static/js/backButton.js @@ -2,8 +2,12 @@ 'use strict'; pandora.ui.backButton = function() { var that = Ox.Button({ - title: Ox._('Back to {0}', [Ox._(pandora.site.itemName.plural)]), - width: 96 + title: Ox._('Back to {0}', [ + pandora.user.ui.section == 'items' + ? Ox._(pandora.site.itemName.plural) + : Ox._(Ox.toTitleCase(pandora.user.ui.section)) + ]), + width: pandora.user.ui.section == 'documents' ? 124 : 96 }).css({ float: 'left', margin: '4px 0 0 4px' @@ -21,7 +25,11 @@ pandora.ui.backButton = function() { if (['accessed', 'timesaccessed'].indexOf(pandora.user.ui.listSort[0].key) > -1) { Ox.Request.clearCache('find'); } - pandora.UI.set({item: ''}); + if (pandora.user.ui.section == 'documents') { + pandora.UI.set({document: ''}); + } else { + pandora.UI.set({item: ''}); + } } }); return that; diff --git a/static/js/collection.js b/static/js/collection.js new file mode 100644 index 00000000..c8a8c280 --- /dev/null +++ b/static/js/collection.js @@ -0,0 +1,289 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; + + +pandora.ui.collection = function() { + + var that, + ui = pandora.user.ui, + view = ui.collectionView, + keys = [ + 'description', 'dimensions', 'extension', 'id', 'title', 'ratio', 'size', 'user', 'entities', 'modified', + 'editable' + ]; + + if (view == 'list') { + that = Ox.TableList({ + keys: keys, + items: function(data, callback) { + pandora.api.findDocuments(Ox.extend(data, { + query: ui.findDocuments + }), callback); + return Ox.clone(data, true); + }, + selected: ui.collectionSelection, + sort: ui.collectionSort.concat([ + {key: 'extension', operator: '+'}, + {key: 'title', operator: '+'} + ]), + unique: 'id', + columns: pandora.site.documentSortKeys.filter(function(key) { + return !key.capability + || pandora.site.capabilities[key.capability][pandora.user.level]; + }).map(function(key) { + var position = ui.collectionColumns.indexOf(key.id); + return { + addable: key.id != 'random', + align: ['string', 'text'].indexOf( + Ox.isArray(key.type) ? key.type[0]: key.type + ) > -1 ? 'left' : key.type == 'list' ? 'center' : 'right', + defaultWidth: key.columnWidth, + format: (function() { + return function(value, data) { + return pandora.formatDocumentKey(key, data); + } + })(), + id: key.id, + operator: pandora.getDocumentSortOperator(key.id), + position: position, + removable: !key.columnRequired, + title: Ox._(key.title), + type: key.type, + visible: position > -1, + width: ui.collectionColumnWidth[key.id] || key.columnWidth + }; + }), + columnsVisible: true, + scrollbarVisible: true, + }) + } else if (view == 'grid') { + that = Ox.IconList({ + borderRadius: 0, + defaultRatio: 640/1024, + draggable: true, + id: 'list', + item: function(data, sort, size) { + var sortKey = sort[0].key, + infoKey = sortKey == 'title' ? 'extension' : sortKey, + key = Ox.getObjectById(pandora.site.documentKeys, infoKey), + info = pandora.formatDocumentKey(key, data), + size = size || 128; + return { + height: Math.round(data.ratio > 1 ? size / data.ratio : size), + id: data.id, + info: info, + title: data.title, + url: pandora.getMediaURL('/documents/' + data.id + '/256p.jpg?' + data.modified), + width: Math.round(data.ratio > 1 ? size : size * data.ratio) + }; + }, + items: function(data, callback) { + pandora.api.findDocuments(Ox.extend(data, { + query: ui.findDocuments + }), callback); + return Ox.clone(data, true); + }, + keys: keys, + selected: ui.collectionSelection, + size: 128, + sort: ui.collectionSort.concat([ + {key: 'extension', operator: '+'}, + {key: 'title', operator: '+'} + ]), + unique: 'id' + }) + .addClass('OxMedia'); + } + + if (['list', 'grid'].indexOf(view) > -1) { + // react to the resize event of the split panel + that.bindEvent({ + resize: function(data) { + that.size(); + }, + pandora_showbrowser: function(data) { + that.size(); + } + }); + } + + if (['list', 'grid'].indexOf(view) > -1) { + + //fixme + + pandora.enableDragAndDrop(that, true); + + that.bindEvent({ + closepreview: function(data) { + pandora.$ui.previewDialog.close(); + delete pandora.$ui.previewDialog; + }, + copy: function(data) { + pandora.clipboard.copy(data.ids, 'document'); + }, + copyadd: function(data) { + pandora.clipboard.add(data.ids, 'document'); + }, + cut: function(data) { + var listData = pandora.getListData(); + if (listData.editable && listData.type == 'static') { + pandora.clipboard.copy(data.ids, 'document'); + pandora.doHistory('cut', data.ids, ui._collection, function() { + pandora.UI.set({collectionSelection: []}); + pandora.reloadList(); + }); + } + }, + cutadd: function(data) { + var listData = pandora.getListData(); + if (listData.editable && listData.type == 'static') { + pandora.clipboard.add(data.ids, 'document'); + pandora.doHistory('cut', data.ids, ui._collection, function() { + pandora.UI.set({collectionSelection: []}); + pandora.reloadList(); + }); + } + }, + 'delete': function(data) { + var listData = pandora.getListData(); + if (listData.editable && listData.type == 'static') { + //fixme use history + //pandora.doHistory('delete', data.ids, ui._collection, function() { + pandora.api.removeCollectionItems({ + collection: ui._collection, + items: data.ids + + }, function() { + pandora.UI.set({collectionSelection: []}); + pandora.reloadList(); + }); + } else if (pandora.user.ui._collection == '' && data.ids.every(function(item) { + return pandora.$ui.list.value(item, 'editable'); + })) { + pandora.ui.deleteDocumentDialog( + data.ids.map(function(id) { + return pandora.$ui.list.value(id); + }), + function() { + Ox.Request.clearCache(); + if (ui.document) { + pandora.UI.set({document: ''}); + } else { + pandora.$ui.list.reloadList() + } + } + ).open(); + } + }, + init: function(data) { + var folder, list; + if (data.query.conditions.length == 0) { + pandora.$ui.allItems.update(data.items); + } else if ( + data.query.conditions.length == 1 + && data.query.conditions[0].key == 'document' + && data.query.conditions[0].operator == '==' + ) { + list = data.query.conditions[0].value; + folder = pandora.getListData(list).folder; + if (pandora.$ui.folderList[folder] + && !Ox.isEmpty(pandora.$ui.folderList[folder].value(list))) { + pandora.$ui.folderList[folder].value( + list, 'items', data.items + ); + } + } + pandora.$ui.statusbar.set('total', data); + data = []; + pandora.site.totals.forEach(function(v) { + data[v.id] = 0; + }); + pandora.$ui.statusbar.set('selected', data); + }, + open: function(data) { + var set = {document: data.ids[0]}; + pandora.UI.set(set); + }, + openpreview: function(data) { + /* + if (!pandora.$ui.previewDialog) { + pandora.$ui.previewDialog = pandora.ui.previewDialog() + .open() + .bindEvent({ + close: function() { + that.closePreview(); + delete pandora.$ui.previewDialog; + } + }); + } else { + pandora.$ui.previewDialog.update(); + } + */ + }, + paste: function(data) { + var items = pandora.clipboard.paste(); + if (items.length && pandora.clipboard.type() == 'document' && pandora.getListData().editable) { + //fixme use history + //pandora.doHistory('paste', items, ui._collection, function() { + pandora.api.addCollectionItems({ + collection: ui._collection, + items: items + + }, function() { + pandora.UI.set({collectionSelection: items}); + pandora.reloadList(); + }); + } + }, + select: function(data) { + var query; + pandora.UI.set('collectionSelection', data.ids); + if (data.ids.length == 0) { + pandora.$ui.statusbar.set('selected', {items: 0}); + } else { + if (Ox.isUndefined(data.rest)) { + query = { + conditions: data.ids.map(function(id) { + return { + key: 'id', + value: id, + operator: '==' + } + }), + operator: '|' + }; + } else { + query = { + conditions: [ui.find].concat( + data.rest.map(function(id) { + return { + key: 'id', + value: id, + operator: '!=' + }; + }) + ), + operator: '&' + }; + } + pandora.api.find({ + query: query + }, function(result) { + pandora.$ui.statusbar.set('selected', result.data); + }); + } + }, + + pandora_collectionsort: function(data) { + that.options({sort: data.value}); + }, + pandora_showdocument: function(data) { + isItemView && that.toggleElement(1); + } + }); + + } + + return that; + +}; diff --git a/static/js/deleteDocumentDialog.js b/static/js/deleteDocumentDialog.js index 7b980744..f8dd62e6 100644 --- a/static/js/deleteDocumentDialog.js +++ b/static/js/deleteDocumentDialog.js @@ -31,7 +31,7 @@ pandora.ui.deleteDocumentDialog = function(files, callback) { }) ], content: files.length == 1 - ? Ox._('Are you sure you want to delete the document "{0}"?', [files[0].name + '.' + files[0].extension]) + ? Ox._('Are you sure you want to delete the document "{0}"?', [files[0].title + '.' + files[0].extension]) : Ox._('Are you sure you want to delete {0} documents?', [files.length]), keys: {enter: 'delete', escape: 'keep'}, title: files.length == 1 diff --git a/static/js/deleteListDialog.js b/static/js/deleteListDialog.js index 8592d527..1b9e6dc2 100644 --- a/static/js/deleteListDialog.js +++ b/static/js/deleteListDialog.js @@ -5,7 +5,7 @@ pandora.ui.deleteListDialog = function(list) { var ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1), listData = pandora.getListData(list), $folderList = pandora.$ui.folderList[listData.folder], @@ -42,6 +42,14 @@ pandora.ui.deleteListDialog = function(list) { pandora.UI.set({ find: pandora.site.user.ui.find }); + } else if (ui.section == 'documents') { + pandora.UI.set( + 'collections.' + listData.id, null + ); + pandora.UI.set({ + findDocuments: pandora.site.user.ui.findDocuments + }); + } else { pandora.UI.set( folderItem.toLowerCase(), '' diff --git a/static/js/document.js b/static/js/document.js new file mode 100644 index 00000000..6e08dca1 --- /dev/null +++ b/static/js/document.js @@ -0,0 +1,282 @@ + +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; + +pandora.ui.document = function() { + var $toolbar = Ox.Bar({size: 16}) + .bindEvent({ + doubleclick: function(e) { + if ($(e.target).is('.OxBar')) { + pandora.$ui.text && pandora.$ui.text.animate({scrollTop:0}, 250); + } + } + }), + $content = Ox.Element(), + that = Ox.SplitPanel({ + elements: [ + { + element: $toolbar, + size: 16 + }, + { + element: $content + } + ], + orientation: 'vertical' + }) + .bindEvent({ + pandora_showbrowser: function(data) { + that.update(); + } + }), + item, + $find, + $nextButton, + $currentButton, + $previousButton; + + pandora.api.getDocument({ + id: pandora.user.ui.document + }, function(result) { + if (pandora.user.ui.document != result.data.id) { + return; + } + item = result.data; + var documentTitle = pandora.getWindowTitle(item); + document.title = pandora.getPageTitle(document.location.pathname) || documentTitle; + pandora.$ui.itemTitle + .options({title: '' + (pandora.getDocumentTitle(item)) + ''}) + .show(); + + if (pandora.user.ui.documentView == 'info') { + $content.replaceWith( + $content = pandora.ui.documentInfoView(result.data) + ); + } else { + setContent(); + } + }); + + function setContent() { + that.replaceElement(1, $content = ( + item.extension == 'pdf' + ? Ox.PDFViewer({ + height: that.height(), + page: pandora.user.ui.documents[item.id] + ? pandora.user.ui.documents[item.id].position + : 1, + url: '/documents/' + item.id + '/' + + item.title + '.' + item.extension, + width: that.width(), + zoom: 'fit' + }) + : item.extension == 'html' + ? pandora.$ui.textPanel = textPanel(item).css({ + }) + : Ox.ImageViewer({ + area: pandora.user.ui.documents[item.id] + ? pandora.user.ui.documents[item.id].position + : [], + height: that.height(), + imageHeight: item.dimensions[1], + imagePreviewURL: pandora.getMediaURL('/documents/' + item.id + '/256p.jpg?' + item.modified), + imageURL: pandora.getMediaURL('/documents/' + item.id + '/' + + item.title + '.' + item.extension + '?' + item.modified), + imageWidth: item.dimensions[0], + width: that.width() + }).css({ + //prevents image overflow on zoom, fixme: fix in Ox.ImageViewer + position: 'absolute' + }) + ) + .bindEvent({ + center: function(data) { + pandora.UI.set( + 'documents.' + item.id, + {position: $content.getArea().map(Math.round)} + ); + }, + key_escape: function() { + // ... + }, + page: function(data) { + pandora.UI.set( + 'documents.' + item.id, + {position: data.page} + ); + }, + zoom: function(data) { + pandora.UI.set( + 'documents.' + item.id, + {position: $content.getArea().map(Math.round)} + ); + } + }) + ); + if (item.extension == 'html') { + that.css({ + 'overflow-y': 'auto' + }); + } + } + + function textPanel(text) { + var textElement, + embedURLs = getEmbedURLs(text.text), + that = Ox.SplitPanel({ + elements: [ + { + element: pandora.$ui.text = textElement = pandora.ui.textHTML(text) + }, + { + element: pandora.$ui.textEmbed = pandora.ui.textEmbed(), + collapsed: !embedURLs.length, + size: pandora.user.ui.embedSize, + resizable: true, + resize: [192, 256, 320, 384, 448, 512] + } + ], + orientation: 'horizontal' + }), + selected = -1, + selectedURL; + /* + $find = Ox.Input({ + clear: true, + placeholder: Ox._('Find in Texts'), + value: pandora.user.ui.textFind, + width: 188 + }) + .css({ + float: 'right', + }) + .bindEvent({ + submit: function(data) { + Ox.print('SUBMIT', data); + } + }) + .appendTo($toolbar); + */ + $nextButton = Ox.Button({ + disabled: embedURLs.length < 2, + title: 'arrowRight', + tooltip: Ox._('Next Reference'), + type: 'image' + }) + .css({ + 'margin-right': (pandora.user.ui.embedSize + Ox.SCROLLBAR_SIZE) + 'px', + float: 'right', + }) + .bindEvent({ + click: function() { + that.selectEmbed( + selected < embedURLs.length - 1 ? selected + 1 : 0, + true + ); + } + }) + .appendTo($toolbar); + + $currentButton = Ox.Button({ + disabled: embedURLs.length < 1, + title: 'center', + tooltip: Ox._('Current Reference'), + type: 'image' + }) + .css({ + float: 'right', + }) + .bindEvent({ + click: scrollToSelectedEmbed + }) + .appendTo($toolbar); + + $previousButton = Ox.Button({ + disabled: embedURLs.length < 2, + title: 'arrowLeft', + tooltip: Ox._('Previous Reference'), + type: 'image' + }) + .css({ + float: 'right', + }) + .bindEvent({ + click: function() { + that.selectEmbed( + selected ? selected - 1 : embedURLs.length - 1, + true + ); + } + }) + .appendTo($toolbar); + + function getEmbedURLs(text) { + var matches = text.match(/]*?href="(.+?)".*?>/gi), + urls = []; + if (matches) { + matches.forEach(function(match) { + var url = match.match(/"(.+?)"/)[1]; + if (pandora.isEmbedURL(url)) { + urls.push(url); + } + }); + } + return urls; + } + + function scrollToSelectedEmbed() { + var scrollTop = Math.max( + textElement[0].scrollTop + $('#embed' + selected).offset().top - ( + pandora.user.ui.showBrowser + ? pandora.$ui.documentContentPanel.options().elements[0].size + : 0 + ) - 48, + 0), + position = 100 * scrollTop / Math.max(1, textElement[0].scrollHeight); + textElement.scrollTo(position); + window.text = textElement; + } + + that.selectEmbed = function(index, scroll) { + if (index != selected) { + selected = index; + selectedURL = embedURLs[selected] + $('.OxSpecialLink').removeClass('OxActive'); + selected > -1 && $('#embed' + selected).addClass('OxActive'); + pandora.$ui.textEmbed.update(selectedURL); + scroll && scrollToSelectedEmbed(); + } + }; + + that.update = function(text) { + var index; + embedURLs = getEmbedURLs(text); + index = embedURLs.indexOf(selectedURL); + if (embedURLs.length && (index == -1 || index >= embedURLs.length)) { + index = 0; + } + selected = -1; + that.selectEmbed(index); + }; + + embedURLs.length && that.selectEmbed(0); + return that; + } + + that.info = function() { + return item; + }; + + that.update = function() { + $content.options({ + height: that.height(), + width: that.width() + }); + $nextButton && $nextButton.css({ + 'margin-right': (pandora.user.ui.embedSize + Ox.SCROLLBAR_SIZE) + 'px', + }); + }; + + return that; + +}; diff --git a/static/js/documentBrowser.js b/static/js/documentBrowser.js new file mode 100644 index 00000000..81e60046 --- /dev/null +++ b/static/js/documentBrowser.js @@ -0,0 +1,94 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; +pandora.ui.documentBrowser = function() { + var that; + if (!pandora.user.ui.document) { + that = Ox.Element().html('fixme'); + } else { + var that = Ox.IconList({ + borderRadius: 0, + centered: true, + defaultRatio: 640/1024, + draggable: true, + id: 'list', + item: function(data, sort, size) { + size = size || 64; + var sortKey = sort[0].key, + infoKey = sortKey == 'title' ? 'extension' : sortKey, + key = Ox.getObjectById(pandora.site.documentKeys, infoKey), + info = pandora.formatDocumentKey(key, data), + size = size || 128; + return { + height: Math.round(data.ratio > 1 ? size / data.ratio : size), + id: data.id, + info: info, + title: data.title, + url: pandora.getMediaURL('/documents/' + data.id + '/256p.jpg?' + data.modified), + width: Math.round(data.ratio > 1 ? size : size * data.ratio) + }; + }, + items: function(data, callback) { + pandora.api.findDocuments(Ox.extend(data, { + query: pandora.user.ui.findDocuments + }), callback); + return Ox.clone(data, true); + }, + keys: ['description', 'dimensions', 'extension', 'id', 'title', 'ratio', 'size', 'user', 'entities', 'modified'], + max: 1, + min: 1, + orientation: 'horizontal', + pageLength: 32, + selected: [pandora.user.ui.document], + size: 64, + sort: getSort(), + unique: 'id' + }) + .addClass('OxMedia') + .bindEvent({ + copy: function() { + pandora.clipboard.copy(pandora.user.ui.item, 'document'); + }, + copyadd: function() { + pandora.clipboard.add(pandora.user.ui.item, 'document'); + }, + gainfocus: function() { + pandora.$ui.mainMenu.replaceItemMenu(); + }, + open: function() { + that.scrollToSelection(); + }, + openpreview: function() { + if (pandora.isVideoView()) { + pandora.$ui[pandora.user.ui.itemView].gainFocus().triggerEvent('key_space'); + } + }, + select: function(data) { + data.ids.length && pandora.UI.set({ + 'document': data.ids[0] + }); + }, + toggle: function(data) { + pandora.UI.set({showBrowser: !data.collapsed}); + } + }) + .bindEventOnce({ + load: function() { + // gain focus if we're on page load or if we've just switched + // to an item and the not-yet-garbage-collected list still has + // focus + if (!Ox.Focus.focusedElement() || ( + pandora.$ui.list && pandora.$ui.list.hasFocus() + )) { + that.gainFocus(); + } + } + }); + that.css({overflowY: 'hidden'}); // this fixes a bug in firefox + pandora.enableDragAndDrop(that, false); + } + function getSort() { + return pandora.user.ui.collectionSort; + } + return that; +}; + diff --git a/static/js/documentContentPanel.js b/static/js/documentContentPanel.js new file mode 100644 index 00000000..254809b8 --- /dev/null +++ b/static/js/documentContentPanel.js @@ -0,0 +1,63 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; + +pandora.ui.documentContentPanel = function() { + var that = Ox.SplitPanel({ + elements: !pandora.user.ui.document ? [ + { + collapsed: true, + collapsible: false, //fixme + element: pandora.$ui.documentBrowser = pandora.ui.documentBrowser(), + resizable: false, //fixme + resize: [96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256], + size: 96, + tooltip: '' /* fixme: + Ox._('filters') + ' ' + + Ox.SYMBOLS.shift + 'F' + */ + }, + { + element: pandora.$ui.list = pandora.ui.collection() + }, + { + element: pandora.$ui.statusbar = pandora.ui.statusbar(), + size: 16 + } + ] : [ + { + collapsed: !pandora.user.ui.showBrowser, + collapsible: true, + element: pandora.$ui.documentBrowser = pandora.ui.documentBrowser(), + size: 112 + Ox.UI.SCROLLBAR_SIZE, + tooltip: Ox._('{0} browser', [Ox._('document')]) + + ' ' + + Ox.SYMBOLS.shift + 'B' + }, + { + element: pandora.$ui.document = pandora.ui.document() + } + ], + orientation: 'vertical' + }) + .bindEvent({ + resize: function(data) { + Ox.print('split resize'); + }, + pandora_document: function(data) { + if (data.value && data.previousValue) { + that.replaceElement(1, pandora.$ui.document = pandora.ui.document()); + } + }, + pandora_documentview: function(data) { + that.replaceElement(1, pandora.$ui.document = pandora.ui.document()); + }, + pandora_collectionview: function() { + !pandora.user.ui.document && that.replaceElement(1, + pandora.$ui.list = pandora.ui.collection()); + }, + pandora_showbrowser: function(data) { + data.value == that.options('elements')[0].collapsed && that.toggleElement(0); + }, + }); + return that; +}; diff --git a/static/js/documentDialog.js b/static/js/documentDialog.js index 84106fb1..b57548bd 100644 --- a/static/js/documentDialog.js +++ b/static/js/documentDialog.js @@ -19,7 +19,7 @@ pandora.openDocumentDialog = function(options) { operator: '|' }, range: [0, options.ids.length], - keys: ['description', 'dimensions', 'extension', 'id', 'name', 'modified'] + keys: ['description', 'dimensions', 'extension', 'id', 'title', 'modified'] }, function(result) { var i = 0, documents = Ox.sort(result.data.items, function(item) { @@ -173,7 +173,7 @@ pandora.ui.documentDialog = function(options) { ? pandora.user.ui.documents[item.id].position : 1, url: '/documents/' + item.id + '/' - + item.name + '.' + item.extension, + + item.title + '.' + item.extension, width: dialogWidth, zoom: 'fit' }) @@ -185,7 +185,7 @@ pandora.ui.documentDialog = function(options) { imageHeight: item.dimensions[1], imagePreviewURL: pandora.getMediaURL('/documents/' + item.id + '/256p.jpg?' + item.modified), imageURL: pandora.getMediaURL('/documents/' + item.id + '/' - + item.name + '.' + item.extension + '?' + item.modified), + + item.title + '.' + item.extension + '?' + item.modified), imageWidth: item.dimensions[0], width: dialogWidth }) @@ -217,7 +217,7 @@ pandora.ui.documentDialog = function(options) { } function setTitle() { - that.options({title: item.name + '.' + item.extension}); + that.options({title: item.title + '.' + item.extension}); } that.getItems = function() { diff --git a/static/js/documentFilterForm.js b/static/js/documentFilterForm.js new file mode 100644 index 00000000..3aae4296 --- /dev/null +++ b/static/js/documentFilterForm.js @@ -0,0 +1,98 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript + +'use strict'; + +pandora.ui.documentFilterForm = function(options) { + // mode can be find, collection, embed + var collection = options.list, + mode = options.mode, + that = Ox.Element(); + + if (mode == 'list') { + mode = 'collection'; + } + + pandora.api.findCollections({ + query: { + conditions: [{key: 'type', value: 'static', operator: '='}], + operator: '&' + }, + keys: ['id'], + range: [0, 1000], + sort: [{key: 'user', operator: '+'}, {key: 'name', operator: '+'}] + }, function(result) { + that.append( + that.$filter = Ox.Filter({ + findKeys: pandora.site.documentKeys.map(function(documentKey) { + var key = Ox.clone(documentKey, true); + key.title = Ox._(key.title); + if (key.format && key.format.type == 'ColorPercent') { + key.format.type = 'percent'; + } + Ox.print(key); + return key; + }).concat([{ + id: 'collection', + title: Ox._('Collection'), + type: 'list', + values: result.data.items.map(function(item) { + return item.id; + }) + }]), + list: mode == 'find' ? { + sort: pandora.user.ui.collectionSort, + view: pandora.user.ui.collectionView + } : null, + sortKeys: pandora.site.documentSortKeys, + value: Ox.clone(mode == 'collection' ? collection.query : pandora.user.ui.documentFind, true), + viewKeys: pandora.site.collectionViews + }) + .css(mode == 'embed' ? {} : {padding: '16px'}) + .bindEvent({ + change: function(data) { + if (mode == 'find') { + if (pandora.user.ui.updateAdvancedFindResults) { + that.updateResults(); + } + } else if (mode == 'collection') { + pandora.api.editCollection({ + id: collection.id, + query: data.value + }, function(result) { + if (pandora.user.ui.updateAdvancedFindResults) { + that.updateResults(); + } + }); + } + that.triggerEvent('change', data); + } + }) + ); + that.getList = that.$filter.getList; + that.value = that.$filter.value; + }); + that.updateResults = function() { + if (mode == 'collection') { + Ox.Request.clearCache(collection.id); + pandora.$ui.list && pandora.$ui.list + .bindEventOnce({ + init: function(data) { + pandora.$ui.folderList[ + pandora.getListData().folder + ].value(collection.id, 'query', that.$filter.options('value')); + } + }) + .reloadList(); + /* + pandora.$ui.filters && pandora.$ui.filters.forEach(function($filter) { + $filter.reloadList(); + }); + */ + } else { + pandora.UI.set({find: Ox.clone(that.$filter.options('value'), true)}); + pandora.$ui.findElement.updateElement(); + } + }; + return that; +}; + diff --git a/static/js/documentInfoView.js b/static/js/documentInfoView.js new file mode 100644 index 00000000..905e1a94 --- /dev/null +++ b/static/js/documentInfoView.js @@ -0,0 +1,572 @@ +'use strict'; + +pandora.ui.documentInfoView = function(data) { + + var ui = pandora.user.ui, + canEdit = pandora.site.capabilities.canEditMetadata[pandora.user.level] || data.editable, + canRemove = pandora.site.capabilities.canRemoveItems[pandora.user.level], + css = { + marginTop: '4px', + textAlign: 'justify' + }, + html, + iconRatio = data.ratio, + iconSize = ui.infoIconSize, + iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio), + iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio), + iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0, + borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8, + margin = 16, + nameKeys = pandora.site.documentKeys.filter(function(key) { + return key.sortType == 'person'; + }).map(function(key) { + return key.id; + }), + listKeys = pandora.site.documentKeys.filter(function(key) { + return Ox.isArray(key.type); + }).map(function(key){ + return key.id; + }), + posterKeys = nameKeys.concat(['title', 'year']), + statisticsWidth = 128, + + $bar = Ox.Bar({size: 16}) + .bindEvent({ + doubleclick: function(e) { + if ($(e.target).is('.OxBar')) { + $info.animate({scrollTop: 0}, 250); + } + } + }), + + $options = Ox.MenuButton({ + items: [ + { + id: 'delete', + title: Ox._('Delete {0}...', [Ox._('Document')]), + disabled: !canRemove + } + ], + style: 'square', + title: 'set', + tooltip: Ox._('Options'), + type: 'image', + }) + .css({ + float: 'left', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data_) { + if (data_.id == 'delete') { + pandora.ui.deleteDocumentDialog( + [data], + function() { + Ox.Request.clearCache(); + if (ui.document) { + pandora.UI.set({document: ''}); + } else { + pandora.$ui.list.reloadList() + } + } + ).open(); + } + } + }) + .appendTo($bar), + + $edit = Ox.MenuButton({ + items: [ + { + id: 'insert', + title: Ox._('Insert HTML...'), + disabled: true + } + ], + style: 'square', + title: 'edit', + tooltip: Ox._('Edit'), + type: 'image', + }) + .css({ + float: 'right', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data) { + // ... + } + }) + .appendTo($bar), + + $info = Ox.Element().css({overflowY: 'auto'}), + + that = Ox.SplitPanel({ + elements: [ + {element: $bar, size: 16}, + {element: $info} + ], + orientation: 'vertical' + }), + + $icon = Ox.Element({ + element: '', + }) + .attr({ + src: '/documents/' + data.id + '/512p.jpg?' + data.modified + }) + .css({ + position: 'absolute', + left: margin + iconLeft + 'px', + top: margin + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px', + cursor: 'pointer' + }) + .bindEvent({ + singleclick: open + }) + .appendTo($info), + + $reflection = $('
') + .addClass('OxReflection') + .css({ + position: 'absolute', + left: margin + 'px', + top: margin + iconHeight + 'px', + width: iconSize + 'px', + height: iconSize / 2 + 'px', + overflow: 'hidden' + }) + .appendTo($info), + + $reflectionIcon = $('') + .attr({ + src: '/documents/' + data.id + '/512p.jpg?' + data.modified + }) + .css({ + position: 'absolute', + left: iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px' + }) + .appendTo($reflection), + + $reflectionGradient = $('
') + .css({ + position: 'absolute', + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }) + .appendTo($reflection), + + $text = Ox.Element() + .addClass('OxTextPage') + .css({ + position: 'absolute', + left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px', + top: margin + 'px', + right: margin + statisticsWidth + margin + 'px', + }) + .appendTo($info), + + $statistics = $('
') + .css({ + position: 'absolute', + width: statisticsWidth + 'px', + top: margin + 'px', + right: margin + 'px' + }) + .appendTo($info), + + $capabilities; + + [$options, $edit].forEach(function($element) { + $element.find('input').css({ + borderWidth: 0, + borderRadius: 0, + padding: '3px' + }); + }); + + listKeys.forEach(function(key) { + if (Ox.isString(data[key])) { + data[key] = [data[key]]; + } + }); + + if (!canEdit) { + pandora.createLinks($info); + } + + // Title ------------------------------------------------------------------- + + $('
') + .css({ + marginTop: '-2px', + }) + .append( + Ox.EditableContent({ + editable: canEdit, + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: data.title || '' + }) + .css({ + marginBottom: '-3px', + fontWeight: 'bold', + fontSize: '13px' + }) + .bindEvent({ + submit: function(event) { + editMetadata('title', event.value); + } + }) + ) + .appendTo($text); + + // Director, Year and Country ---------------------------------------------- + + renderGroup(['author', 'type', 'date']); + renderGroup(['publisher', 'place', 'series', 'edition']); + renderGroup(['language']); + renderGroup(['extension', 'dimensions', 'size', 'user', 'matches']); + + + // Description ----------------------------------------------------------------- + + if (canEdit || data.description) { + $('
') + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + format: function(value) { + return value.replace( + /') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + $('
').html(Ox.formatDate(data[key], '%B %e, %Y')) + ) + .appendTo($statistics); + }); + $('
') + .css({marginBottom: '4px'}) + .append(formatKey('timesaccessed', 'statistics')) + .append( + $('
').html(data.timesaccessed) + ) + .appendTo($statistics); + + // Rights Level ------------------------------------------------------------ + + /* + var $rightsLevel = $('
'); + $('
') + .css({marginBottom: '4px'}) + .append(formatKey('Rights Level', 'statistics')) + .append($rightsLevel) + .appendTo($statistics); + renderRightsLevel(); + */ + + + function editMetadata(key, value) { + if (value != data[key]) { + var edit = {id: data.id}; + if (key == 'title') { + edit[key] = value; + } else if (listKeys.indexOf(key) >= 0) { + edit[key] = value ? value.split(', ') : []; + } else { + edit[key] = value ? value : null; + } + pandora.api.editDocument(edit, function(result) { + var src; + data[key] = result.data[key]; + Ox.Request.clearCache(); // fixme: too much? can change filter/list etc + if (result.data.id != data.id) { + pandora.UI.set({document: result.data.id}); + pandora.$ui.browser.value(data.id, 'id', result.data.id); + } + //pandora.updateItemContext(); + //pandora.$ui.browser.value(result.data.id, key, result.data[key]); + pandora.$ui.itemTitle + .options({title: '' + (pandora.getDocumentTitle(result.data)) + ''}); + }); + } + } + + function formatKey(key, mode) { + var item = Ox.getObjectById(pandora.site.itemKeys, key); + key = Ox._(item ? item.title : key); + mode = mode || 'text'; + return mode == 'text' + ? '' + Ox.toTitleCase(key) + ': ' + : mode == 'description' + ? Ox.toTitleCase(key) + : Ox.Element() + .css({marginBottom: '4px', fontWeight: 'bold'}) + .html(Ox.toTitleCase(key) + .replace(' Per ', ' per ')); + } + + function formatLight(str) { + return '' + str + ''; + } + + function formatLink(value, key) { + return (Ox.isArray(value) ? value : [value]).map(function(value) { + return key + ? '' + value + '' + : value; + }).join(', '); + } + + function formatValue(key, value) { + var ret; + if (key == 'date') { + ret = value ? Ox.formatDate(value, + ['', '%Y', '%B %Y', '%B %e, %Y'][value.split('-').length], + true + ) : ''; + } else if (nameKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), key); + } else if (listKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), key); + } else if (['type'].indexOf(key) > -1) { + ret = formatLink(value, key); + } else { + ret = pandora.formatDocumentKey(Ox.getObjectById(pandora.site.documentKeys, key), data); + } + return ret; + } + + function getRightsLevelElement(rightsLevel) { + return Ox.Theme.formatColorLevel( + rightsLevel, + pandora.site.rightsLevels.map(function(rightsLevel) { + return rightsLevel.name; + }) + ); + } + + function getValue(key, value) { + return !value ? '' + : Ox.contains(listKeys, key) ? value.join(', ') + : value; + } + + function renderCapabilities(rightsLevel) { + var capabilities = [].concat( + canEdit ? [{name: 'canSeeItem', symbol: 'Find'}] : [], + [ + {name: 'canPlayClips', symbol: 'PlayInToOut'}, + {name: 'canPlayVideo', symbol: 'Play'}, + {name: 'canDownloadVideo', symbol: 'Download'} + ] + ), + userLevels = canEdit ? pandora.site.userLevels : [pandora.user.level]; + $capabilities.empty(); + userLevels.forEach(function(userLevel, i) { + var $element, + $line = $('
') + .css({ + height: '16px', + marginBottom: '4px' + }) + .appendTo($capabilities); + if (canEdit) { + $element = Ox.Theme.formatColorLevel(i, userLevels.map(function(userLevel) { + return Ox.toTitleCase(userLevel); + }), [0, 240]); + Ox.Label({ + textAlign: 'center', + title: Ox.toTitleCase(userLevel), + width: 60 + }) + .addClass('OxColor OxColorGradient') + .css({ + float: 'left', + height: '12px', + paddingTop: '2px', + background: $element.css('background'), + fontSize: '8px', + color: $element.css('color') + }) + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + } + capabilities.forEach(function(capability) { + var hasCapability = pandora.site.capabilities[capability.name][userLevel] >= rightsLevel, + $element = Ox.Theme.formatColorLevel(hasCapability, ['', '']); + Ox.Button({ + tooltip: (canEdit ? Ox.toTitleCase(userLevel) : 'You') + ' ' + + (hasCapability ? 'can' : 'can\'t') + ' ' + + Ox.toSlashes(capability.name) + .split('/').slice(1).join(' ') + .toLowerCase(), + title: capability.symbol, + type: 'image' + }) + .addClass('OxColor OxColorGradient') + .css({background: $element.css('background')}) + .css('margin' + (canEdit ? 'Left' : 'Right'), '4px') + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + }); + if (!canEdit) { + Ox.Button({ + title: Ox._('Help'), + tooltip: Ox._('About Rights'), + type: 'image' + }) + .css({marginLeft: '52px'}) + .bindEvent({ + click: function() { + pandora.UI.set({page: 'rights'}); + } + }) + .appendTo($line); + } + }); + } + + function renderGroup(keys) { + var $element; + if (canEdit || keys.filter(function(key) { + return data[key]; + }).length) { + $element = $('
').addClass('OxSelectable').css(css); + keys.forEach(function(key, i) { + if (canEdit || data[key]) { + if ($element.children().length) { + $('').html('; ').appendTo($element); + } + $('').html(formatKey(key)).appendTo($element); + Ox.EditableContent({ + clickLink: pandora.clickLink, + format: function(value) { + return formatValue(key, value); + }, + placeholder: formatLight(Ox._('unknown')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: getValue(key, data[key]) + }) + .bindEvent({ + submit: function(data) { + editMetadata(key, data.value); + } + }) + .appendTo($element); + } + }); + $element.appendTo($text); + } + } + + function renderRightsLevel() { + var $rightsLevelElement = getRightsLevelElement(data.rightslevel), + $rightsLevelSelect; + $rightsLevel.empty(); + if (canEdit) { + $rightsLevelSelect = Ox.Select({ + items: pandora.site.rightsLevels.map(function(rightsLevel, i) { + return {id: i, title: rightsLevel.name}; + }), + width: 128, + value: data.rightslevel + }) + .addClass('OxColor OxColorGradient') + .css({ + marginBottom: '4px', + background: $rightsLevelElement.css('background') + }) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + .bindEvent({ + change: function(event) { + var rightsLevel = event.value; + $rightsLevelElement = getRightsLevelElement(rightsLevel); + $rightsLevelSelect + .css({background: $rightsLevelElement.css('background')}) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + renderCapabilities(rightsLevel); + pandora.api.editDocument({id: data.id, rightslevel: rightsLevel}, function(result) { + // ... + }); + } + }) + .appendTo($rightsLevel); + } else { + $rightsLevelElement + .css({ + marginBottom: '4px' + }) + .appendTo($rightsLevel); + } + $capabilities = $('
').appendTo($rightsLevel); + renderCapabilities(data.rightslevel); + } + + function open() { + pandora.UI.set({ + documentView: 'view', + }); + } + + that.reload = function() { + var src = '/documents/' + data.id + '/512p.jpg?' + data.modified; + $icon.attr({src: src}); + $reflectionIcon.attr({src: src}); + iconSize = iconSize == 256 ? 512 : 256; + iconRatio = ui.icons == 'posters' + ? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1; + toggleIconSize(); + }; + + that.bindEvent({ + mousedown: function() { + setTimeout(function() { + !Ox.Focus.focusedElementIsInput() && that.gainFocus(); + }); + }, + pandora_icons: that.reload, + pandora_showsiteposters: function() { + ui.icons == 'posters' && that.reload(); + } + }); + + return that; + +}; diff --git a/static/js/documentPanel.js b/static/js/documentPanel.js new file mode 100644 index 00000000..d04e9540 --- /dev/null +++ b/static/js/documentPanel.js @@ -0,0 +1,30 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript + +'use strict'; + +pandora.ui.documentPanel = function() { + var that = Ox.SplitPanel({ + elements: [ + { + element: pandora.$ui.toolbar = pandora.ui.documentToolbar(), + size: 24 + }, + { + element: pandora.$ui.documentContentPanel = pandora.ui.documentContentPanel() + } + ], + id: 'documentPanel', + orientation: 'vertical' + }) + .bindEvent({ + resize: function(data) { + if (!pandora.user.ui.document) { + pandora.$ui.list && pandora.$ui.list.size(); + } else { + pandora.$ui.document && pandora.$ui.document.update(); + } + }, + }); + return that; +}; + diff --git a/static/js/documentToolbar.js b/static/js/documentToolbar.js new file mode 100644 index 00000000..841ebbb5 --- /dev/null +++ b/static/js/documentToolbar.js @@ -0,0 +1,208 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript + +'use strict'; + +pandora.ui.documentToolbar = function() { + var ui = pandora.user.ui, + isNavigationView = !ui.item, + that = Ox.Bar({ + size: 24 + }).css({ + zIndex: 2 // fixme: remove later + }), + $viewSelect, + $sortView; + + ui.document && that.append( + pandora.$ui.backButton = pandora.ui.backButton() + ); + + $viewSelect = documentViewSelect().appendTo(that); + if (!ui.document) { + $sortView = documentSortSelect().appendTo(that); + } + + that.append( + !ui.document + ? pandora.$ui.listTitle = Ox.Label({ + textAlign: 'center', + title: getCollectionName(pandora.user.ui._collection) + }) + .addClass('OxSelectable') + .css({ + position: 'absolute', + left: getListTitleLeft() + 'px', + top: '4px', + right: (ui._collection ? 340 : 326) + 'px', + width: 'auto' + }) + : pandora.$ui.itemTitle = Ox.Label({ + textAlign: 'center' + }) + .addClass('OxSelectable') + .css({ + position: 'absolute', + left: '266px', + top: '4px', + right: (ui._collection ? 340 : 326) + 'px', + width: 'auto' + }) + .hide() + ); + (!ui.document ? pandora.$ui.listTitle : pandora.$ui.itemTitle).bindEvent({ + doubleclick: function() { + if (!ui.document) { + pandora.$ui.list && ( + ui.collectionView == 'list' + ? pandora.$ui.list.$body + : pandora.$ui.list + ).animate({ + scrollTop: 0 + }, 250); + } else { + //fixme: + pandora.$ui.browser.scrollToSelection(); + } + } + }) + that.append( + pandora.$ui.findDocumentsElement = pandora.ui.findDocumentsElement(function(data) { + var key = data.key, + value = data.value, + conditions; + if (key == 'all') { + key = 'title' + } + conditions = [ + {key: key, operator: '=', value: value} + ]; + if (pandora.user.ui._collection) { + conditions.push({ + key: 'collection', + value: pandora.user.ui._collection, + operator: '==' + }); + } + pandora.UI.set({findDocuments: {conditions: conditions, operator: '&'}}); + }) + ); + that.bindEvent({ + pandora_collectionsort: function(data) { + $sortView.updateElement(); + }, + pandora_documentview: function(data) { + $viewSelect.options({ + value: data.value + }); + } + }); + function getCollectionName(listId) { + return '' + ( + listId == '' + ? Ox._('All {0}', [Ox._(Ox.toTitleCase(ui.section))]) + : Ox.encodeHTMLEntities(listId.slice(listId.indexOf(':') + 1)) + ) + ''; + } + function getListTitleLeft() { + return 284; + } + + function documentSortSelect() { + var $orderButton = Ox.Button({ + overlap: 'left', + title: getOrderButtonTitle(), + tooltip: getOrderButtonTooltip(), + type: 'image' + }) + .bindEvent({ + click: function() { + pandora.UI.set({collectionSort: [{ + key: ui.collectionSort[0].key, + operator: ui.collectionSort[0].operator == '+' ? '-' : '+' + }]}); + }, + }), + $sortSelect = Ox.Select({ + items: pandora.site.documentKeys.filter(function(key) { + return key.sort; + }).map(function(key) { + return { + id: key.id, + title: Ox._('Sort by {0}', [key.title]) + }; + }), + value: ui.collectionSort[0].key, + width: 128 + }) + .bindEvent({ + change: function(data) { + var key = data.value; + pandora.UI.set({collectionSort: [{ + key: key, + operator: pandora.getDocumentSortOperator(key) + }]}); + } + }), + that = Ox.FormElementGroup({ + elements: [$sortSelect, $orderButton], + float: 'right' + }) + .css({float: 'left', margin: '4px 2px'}) + + function getOrderButtonTitle() { + return ui.collectionSort[0].operator == '+' ? 'up' : 'down'; + } + + function getOrderButtonTooltip() { + return Ox._(ui.collectionSort[0].operator == '+' ? 'Ascending' : 'Descending'); + } + + that.updateElement = function() { + $sortSelect.value(ui.collectionSort[0].key); + $orderButton.options({ + title: getOrderButtonTitle(), + tooltip: getOrderButtonTooltip() + }); + }; + return that; + } + + function documentViewSelect() { + var that; + if (!ui.document) { + that = Ox.Select({ + items: pandora.site.collectionViews, + value: ui.collectionView, + width: 128 + }) + .css({float: 'left', margin: '4px 2px 4px 4px'}) + .bindEvent({ + change: function(data) { + pandora.UI.set({collectionView: data.value}); + } + }); + } else { + that = Ox.Select({ + items: [ + {id: 'info', title: Ox._('View Info')}, + {id: 'view', title: Ox._('View Document')} + ], + value: ui.documentView, + width: 128 + }) + .css({float: 'left', margin: '4px 2px 4px 4px'}) + .bindEvent({ + change: function(data) { + pandora.UI.set({documentView: data.value}); + }, + }); + } + return that; + } + + that.updateListName = function(listId) { + pandora.$ui.listTitle.options({title: getCollectionName(listId)}); + }; + return that; +}; + diff --git a/static/js/documentsPanel.js b/static/js/documentsPanel.js index 88005b77..265f8b66 100644 --- a/static/js/documentsPanel.js +++ b/static/js/documentsPanel.js @@ -2,151 +2,108 @@ 'use strict'; -pandora.ui.documentsPanel = function(options) { +pandora.documentColumns = [ + { + id: 'title', + operator: '+', + title: Ox._('Title'), + find: true, + visible: true, + width: 256 + }, + { + id: 'id', + operator: '+', + title: Ox._('ID'), + visible: true, + width: 64 + }, + { + format: function(value) { + return value.toUpperCase(); + }, + id: 'extension', + operator: '+', + title: Ox._('Extension'), + find: true, + visible: true, + width: 64 + }, + { + align: 'right', + format: function(value, data) { + return Ox.isArray(value) + ? Ox.formatDimensions(value, 'px') + : Ox.formatCount(value, data.extension == 'html' ? 'word' : 'page'); + }, + id: 'dimensions', + operator: '-', + title: Ox._('Dimensions'), + visible: true, + width: 128 + }, + { + align: 'right', + format: function(value) { + return Ox.formatValue(value, 'B'); + }, + id: 'size', + operator: '-', + title: Ox._('Size'), + visible: true, + width: 64 + }, + { + id: 'description', + operator: '+', + title: Ox._('Description'), + find: true, + visible: true, + width: 256 + }, + { + align: 'right', + id: 'matches', + operator: '-', + title: Ox._('Matches'), + visible: true, + width: 64 + }, + { + id: 'user', + operator: '+', + title: Ox._('User'), + find: true, + visible: true, + width: 128 + }, + { + align: 'right', + format: function(value) { + return Ox.formatDate(value, '%F %T'); + }, + id: 'created', + operator: '-', + title: Ox._('Created'), + visible: true, + width: 144 + }, + { + align: 'right', + format: function(value) { + return Ox.formatDate(value, '%F %T'); + }, + id: 'modified', + operator: '-', + title: Ox._('Modified'), + visible: true, + width: 144 + } +]; +pandora.ui.documentSortSelect = function() { var ui = pandora.user.ui, - hasItemView = ui.section == 'items' && ui.item, - hasListSelection = ui.section == 'items' && !ui.item && ui.listSelection.length, - isItemView = options.isItemView, - listLoaded = false, - allFindKeys = ['user', 'name', 'entity', 'extension', 'description'].filter(function(key) { - return key != 'entity' || pandora.site.entities.length; - }), - - columns = [ - { - id: 'name', - operator: '+', - title: Ox._('Name'), - visible: true, - width: 256 - }, - { - id: 'id', - operator: '+', - title: Ox._('ID'), - visible: true, - width: 64 - }, - { - format: function(value) { - return value.toUpperCase(); - }, - id: 'extension', - operator: '+', - title: Ox._('Extension'), - visible: true, - width: 64 - }, - { - align: 'right', - format: function(value) { - return Ox.isArray(value) - ? Ox.formatDimensions(value, 'px') - : Ox.formatCount(value, 'page'); - }, - id: 'dimensions', - operator: '-', - title: Ox._('Dimensions'), - visible: true, - width: 128 - }, - { - align: 'right', - format: function(value) { - return Ox.formatValue(value, 'B'); - }, - id: 'size', - operator: '-', - title: Ox._('Size'), - visible: true, - width: 64 - }, - { - id: 'description', - operator: '+', - title: Ox._('Description'), - visible: true, - width: 256 - }, - { - align: 'right', - id: 'matches', - operator: '-', - title: Ox._('Matches'), - visible: true, - width: 64 - }, - { - id: 'user', - operator: '+', - title: Ox._('User'), - visible: true, - width: 128 - }, - { - align: 'right', - format: function(value) { - return Ox.formatDate(value, '%F %T'); - }, - id: 'created', - operator: '-', - title: Ox._('Created'), - visible: true, - width: 144 - }, - { - align: 'right', - format: function(value) { - return Ox.formatDate(value, '%F %T'); - }, - id: 'modified', - operator: '-', - title: Ox._('Modified'), - visible: true, - width: 144 - } - ], - - $listBar = Ox.Bar({size: 24}), - - $viewSelect = Ox.Select({ - items: [ - {id: 'list', title: Ox._('View as List')}, - {id: 'grid', title: Ox._('View as Grid')} - ], - value: ui.documentsView, - width: 128 - }) - .css({float: 'left', margin: '4px 2px 4px 4px'}) - .bindEvent({ - change: function(data) { - pandora.UI.set({documentsView: data.value}); - } - }) - .appendTo($listBar), - - $sortSelect = Ox.Select({ - items: columns.map(function(column) { - return { - id: column.id, - title: Ox._('Sort by {0}', [column.title]) - }; - }), - value: ui.documentsSort[0].key, - width: 128 - }) - .bindEvent({ - change: function(data) { - var key = data.value; - pandora.UI.set({documentsSort: [{ - key: key, - operator: Ox.getObjectById(columns, key).operator - }]}); - } - }), - - $orderButton = Ox.Button({ + $orderButton = Ox.Button({ overlap: 'left', title: getOrderButtonTitle(), tooltip: getOrderButtonTooltip(), @@ -160,55 +117,85 @@ pandora.ui.documentsPanel = function(options) { }]}); } }), - - $sortElement = Ox.FormElementGroup({ - elements: [$sortSelect, $orderButton], - float: 'right' - }) - .css({float: 'left', margin: '4px 2px'}) - .appendTo($listBar), - - $findSelect = Ox.Select({ - items: [ - {id: 'all', title: Ox._('Find: All')}, - {id: 'name', title: Ox._('Find: Name')}, - {id: 'user', title: Ox._('Find: User')}, - {id: 'entity', title: Ox._('Find: Entity')} - ].filter(function(item) { - if (item.id == 'user') { - return !isItemView; - } else if (item.id == 'entity') { - return pandora.site.entities.length; - } - return true; + $sortSelect = Ox.Select({ + items: pandora.documentColumns.map(function(column) { + return { + id: column.id, + title: Ox._('Sort by {0}', [column.title]) + }; }), - overlap: 'right', - type: 'image' + value: ui.documentsSort[0].key, + width: 128 }) .bindEvent({ change: function(data) { - $findInput.options({placeholder: data.title}).focusInput(); + var key = data.value; + pandora.UI.set({documentsSort: [{ + key: key, + operator: Ox.getObjectById(pandora.documentColumns, key).operator + }]}); } }), - - $findInput = Ox.Input({ - changeOnKeypress: true, - clear: true, - placeholder: Ox._('Find: All'), - width: 192 + that = Ox.FormElementGroup({ + elements: [$sortSelect, $orderButton], + float: 'right' }) + .css({float: 'left', margin: '4px 2px'}); + + function getOrderButtonTitle() { + return ui.documentsSort[0].operator == '+' ? 'up' : 'down'; + } + + function getOrderButtonTooltip() { + return Ox._(ui.documentsSort[0].operator == '+' ? 'Ascending' : 'Descending'); + } + + that.sortValue = function(value) { + $sortSelect.value(value); + $orderButton.options({ + title: getOrderButtonTitle(), + tooltip: getOrderButtonTooltip() + }); + }; + return that; +}; + +pandora.ui.documentViewSelect = function() { + var ui = pandora.user.ui, + that = Ox.Select({ + items: [ + {id: 'list', title: Ox._('View as List')}, + {id: 'grid', title: Ox._('View as Grid')} + ], + value: ui.documentsView, + width: 128 + }) + .css({float: 'left', margin: '4px 2px 4px 4px'}) .bindEvent({ - change: updateList + change: function(data) { + pandora.UI.set({documentsView: data.value}); + } + }); + return that; +}; + +pandora.ui.documentsPanel = function(options) { + + var ui = pandora.user.ui, + hasItemView = ui.section == 'items' && ui.item, + hasListSelection = ui.section == 'items' && !ui.item && ui.listSelection.length, + isItemView = options.isItemView, + listLoaded = false, + allFindKeys = ['user', 'title', 'entity', 'extension', 'description'].filter(function(key) { + return key != 'entity' || pandora.site.entities.length; }), - $findElement = Ox.FormElementGroup({ - elements: [ - $findSelect, - $findInput - ] - }) - .css({float: 'right', margin: '4px 4px 4px 2px'}) - .appendTo($listBar), + $listBar = Ox.Bar({size: 24}), + + $viewSelect = pandora.ui.documentViewSelect().appendTo($listBar), + $sortElement = pandora.ui.documentSortSelect().appendTo($listBar), + + $findElement = findElement(updateList, isItemView).appendTo($listBar), $list = renderList(), @@ -389,7 +376,7 @@ pandora.ui.documentsPanel = function(options) { that.size(1, data.value); }, pandora_documentssort: function(data) { - updateSortElement(); + $sortElement.sortValue(ui.documentsSort[0].key); $list.options({sort: data.value}); }, pandora_documentsview: function(data) { @@ -410,12 +397,6 @@ pandora.ui.documentsPanel = function(options) { pandora.$ui.documentsList = $list; } - // to determine the width of the find input inside - // the documents dialog, that dialog has to be present - setTimeout(function() { - $findInput.options({width: getFindInputWidth()}); - }); - function addDocuments() { var ids = ui.documentsSelection['']; pandora.api.addDocument({ @@ -460,12 +441,59 @@ pandora.ui.documentsPanel = function(options) { openDocumentsDialog(); } - function getOrderButtonTitle() { - return ui.documentsSort[0].operator == '+' ? 'up' : 'down'; - } + function findElement(callback, isItemView) { + var $findSelect = Ox.Select({ + items: [ + {id: 'all', title: Ox._('Find: All')}, + {id: 'title', title: Ox._('Find: Title')}, + {id: 'user', title: Ox._('Find: User')}, + {id: 'entity', title: Ox._('Find: Entity')} + ].filter(function(item) { + if (item.id == 'user') { + return !isItemView; + } else if (item.id == 'entity') { + return pandora.site.entities.length; + } + return true; + }), + overlap: 'right', + type: 'image' + }) + .bindEvent({ + change: function(data) { + $findInput.options({placeholder: data.title}).focusInput(); + } + }), - function getOrderButtonTooltip() { - return Ox._(ui.documentsSort[0].operator == '+' ? 'Ascending' : 'Descending'); + $findInput = Ox.Input({ + changeOnKeypress: true, + clear: true, + placeholder: Ox._('Find: All'), + width: 192 + }) + .bindEvent({ + change: function(data) { + data.key = $findSelect.value(); + data.value = $findInput.value() + callback(data); + } + }), + + that = Ox.FormElementGroup({ + elements: [ + $findSelect, + $findInput + ] + }) + .css({float: 'right', margin: '4px 4px 4px 2px'}); + + // to determine the width of the find input inside + // the documents dialog, that dialog has to be present + setTimeout(function() { + $findInput.options({width: getFindInputWidth()}); + }); + + return that; } function getFindInputWidth() { @@ -564,8 +592,8 @@ pandora.ui.documentsPanel = function(options) { .append( $name = Ox.EditableContent({ editable: editable, - tooltip: editable ? pandora.getEditTooltip('name') : '', - value: item.name, + tooltip: editable ? pandora.getEditTooltip('title') : '', + value: item.title, width: width }) .css({ @@ -580,11 +608,11 @@ pandora.ui.documentsPanel = function(options) { }, submit: function(data) { pandora.api.editDocument({ - name: data.value, + title: data.value, id: item.id, item: ui.item, }, function(result) { - $name.options({value: result.data.name}); + $name.options({value: result.data.title}); Ox.Request.clearCache('findDocuments'); $list.reloadList(); }); @@ -631,10 +659,10 @@ pandora.ui.documentsPanel = function(options) { items: [ Ox.Input({ disabled: !editable, - id: 'name', - label: Ox._('Name'), + id: 'title', + label: Ox._('Title'), labelWidth: labelWidth, - value: item.name, + value: item.title, width: width }), Ox.Input({ @@ -839,7 +867,7 @@ pandora.ui.documentsPanel = function(options) { function renderList() { var options = { items: pandora.api.findDocuments, - keys: ['description', 'dimensions', 'extension', 'id', 'name', 'ratio', 'size', 'user', 'entities', 'modified'], + keys: ['description', 'dimensions', 'extension', 'id', 'title', 'ratio', 'size', 'user', 'entities', 'modified'], query: { conditions: isItemView ? [{ key: 'item', value: ui.item, operator: '==' }] : [], operator: '&' @@ -847,27 +875,27 @@ pandora.ui.documentsPanel = function(options) { selected: ui.documentsSelection[isItemView ? ui.item : ''] || [], sort: ui.documentsSort.concat([ {key: 'extension', operator: '+'}, - {key: 'name', operator: '+'} + {key: 'title', operator: '+'} ]), unique: 'id' }; return (ui.documentsView == 'list' ? Ox.TableList(Ox.extend(options, { - columns: columns, + columns: pandora.documentColumns, columnsVisible: true, scrollbarVisible: true, })) : Ox.IconList(Ox.extend(options, { item: function(data, sort, size) { var sortKey = sort[0].key, - infoKey = sortKey == 'name' ? 'extension' : sortKey, + infoKey = sortKey == 'title' ? 'extension' : sortKey, info = ( - Ox.getObjectById(columns, infoKey).format || Ox.identity + Ox.getObjectById(pandora.documentColumns, infoKey).format || Ox.identity )(data[infoKey]), size = size || 128; return { height: Math.round(data.ratio > 1 ? size / data.ratio : size), id: data.id, info: info, - title: data.name, + title: data.title, url: pandora.getMediaURL('/documents/' + data.id + '/256p.jpg?' + data.modified), width: Math.round(data.ratio > 1 ? size : size * data.ratio) }; @@ -879,6 +907,9 @@ pandora.ui.documentsPanel = function(options) { // we can't open upload dialog via control+n isItemView && openDocumentsDialog(); }, + copy: function(data) { + pandora.clipboard.copy(data.ids, 'document'); + }, closepreview: closeDocuments, 'delete': isItemView ? removeDocuments : deleteDocuments, init: function(data) { @@ -892,6 +923,22 @@ pandora.ui.documentsPanel = function(options) { }, open: openDocuments, openpreview: openDocuments, + paste: function(data) { + if (isItemView) { + //fixme permissions! + var items = pandora.clipboard.paste(); + if (items.length && pandora.clipboard.type() == 'document') { + //fixme use history + pandora.api.addDocument({ + item: ui.item, + ids: items + }, function(result) { + Ox.Request.clearCache('findDocuments'); + $list.reloadList(); + }); + } + } + }, select: function(data) { pandora.UI.set( 'documentsSelection.' + (isItemView ? ui.item : ''), data.ids @@ -1017,9 +1064,10 @@ pandora.ui.documentsPanel = function(options) { ); } - function updateList() { - var key = $findSelect.value(), - value = $findInput.value(), + function updateList(data) { + + var key = data.key, + value = data.value, itemCondition = isItemView ? {key: 'item', operator: '==', value: ui.item} : null, @@ -1040,11 +1088,7 @@ pandora.ui.documentsPanel = function(options) { } function updateSortElement() { - $sortSelect.value(ui.documentsSort[0].key); - $orderButton.options({ - title: getOrderButtonTitle(), - tooltip: getOrderButtonTooltip() - }); + $sortElement.sortValue(ui.documentsSort[0].key); } function uploadDocuments(data) { diff --git a/static/js/findDocumentsElement.js b/static/js/findDocumentsElement.js new file mode 100644 index 00000000..be856bdc --- /dev/null +++ b/static/js/findDocumentsElement.js @@ -0,0 +1,159 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; + +pandora.ui.findDocumentsElement = function() { + var findIndex = pandora.user.ui._findDocumentsState.index, + findKey = pandora.user.ui._findDocumentsState.key, + findValue = pandora.user.ui._findDocumentsState.value, + hasPressedClear = false, + previousFindKey = findKey, + $findCollectionSelect, + $findSelect, + $findInput, + that = Ox.FormElementGroup({ + elements: [].concat(pandora.user.ui._collection ? [ + $findCollectionSelect = Ox.Select({ + items: [ + {id: 'all', title: Ox._('Find: All {0}', [Ox._('Documents')])}, + {id: 'collection', title: Ox._('Find: This Collection')} + ], + overlap: 'right', + type: 'image', + tooltip: Ox._('Find: This Collection'), + value: 'collection' + }) + .bindEvent({ + change: function(data) { + $findCollectionSelect.options({ + tooltip: Ox.getObjectById( + $findCollectionSelect.options('items'), + data.value + ).title + }); + $findInput.focusInput(true); + } + }), + ] : [], [ + $findSelect = Ox.Select({ + id: 'select', + items: pandora.site.documentKeys.filter(function(key) { + return key.find; + }).map(function(key) { + return { + id: key.id, + title: Ox._('Find: {0}', [Ox._(key.title)]) + }; + }), + overlap: 'right', + value: findKey, + width: 128 + }) + .bindEvent({ + change: function(data) { + //pandora.$ui.mainMenu.checkItem('findMenu_find_' + data.value); + $findInput.options({ + autocomplete: autocompleteFunction(), + placeholder: '' + }).focusInput(true); + previousFindKey = data.value; + } + }), + $findInput = Ox.Input({ + autocomplete: autocompleteFunction(), + autocompleteSelect: true, + autocompleteSelectHighlight: true, + autocompleteSelectMaxWidth: 256, + autocompleteSelectSubmit: true, + clear: true, + clearTooltip: Ox._('Click to clear or doubleclick to reset query'), + id: 'input', + placeholder: findKey == 'advanced' ? Ox._('Edit Query...') : '', + value: findValue, + width: 192 + }) + .bindEvent({ + clear: function() { + hasPressedClear = true; + }, + focus: function(data) { + if ($findSelect.value() == 'advanced') { + if (hasPressedClear) { + pandora.UI.set({find: pandora.site.user.ui.find}); + that.updateElement(); + hasPressedClear = false; + } + $findInput.blurInput(); + //fixme advanced find dialog for documents + //pandora.$ui.filterDialog = pandora.ui.filterDialog().open(); + } + }, + submit: function(data) { + var findInList = pandora.user.ui._collection + && $findCollectionSelect.value() == 'collection', + key = $findSelect.value(), + conditions = [].concat( + findInList ? [{ + key: 'collection', + value: pandora.user.ui._collection, + operator: '==' + }] : [], + data.value ? [{ + key: key, + value: data.value, + operator: '=' + }] : [] + ); + pandora.UI.set({ + findDocuments: {conditions: conditions, operator: '&'} + }); + } + }) + ]), + id: 'findElement' + }) + .css({ + float: 'right', + margin: '4px' + }); + function autocompleteFunction() { + var key = !that + ? pandora.user.ui._findDocumentsState.key + : that.value()[pandora.user.ui._collection ? 1 : 0], + findKey = Ox.getObjectById(pandora.site.documentFindKeys, key); + return findKey && findKey.autocomplete ? function(value, callback) { + value === '' && Ox.Log('', 'Warning: autocomplete function should never be called with empty value'); + pandora.api.autocompleteDocuments({ + key: key, + query: { + conditions: pandora.user.ui._collection + && $findCollectionSelect.value() == 'collection' + ? [{key: 'collection', value: pandora.user.ui._collection, operator: '=='}] : [], + operator: '&' + }, + range: [0, 20], + sort: findKey.autocompleteSort, + value: value + }, function(result) { + callback(result.data.items.map(function(item) { + return Ox.decodeHTMLEntities(item); + })); + }); + } : null; + } + that.updateElement = function() { + var findState = pandora.user.ui._findDocumentsState; + $findSelect.value(findState.key); + $findInput.options( + findState.key == 'advanced' ? { + placeholder: Ox._('Edit Query...'), + value: '' + } : { + autocomplete: autocompleteFunction(), + placeholder: '', + value: findState.value + } + ); + }; + return that; +}; + diff --git a/static/js/folderBrowserBar.js b/static/js/folderBrowserBar.js index bbf4409b..26cd5f55 100644 --- a/static/js/folderBrowserBar.js +++ b/static/js/folderBrowserBar.js @@ -3,7 +3,7 @@ pandora.ui.folderBrowserBar = function(id, section) { section = section || pandora.user.ui.section; var ui = pandora.user.ui, - folderItems = section == 'items' ? 'Lists' : Ox.toTitleCase(section), + folderItems = pandora.getFolderItems(section), folderItem = folderItems.slice(0, -1), that = Ox.Bar({ size: 24 diff --git a/static/js/folderBrowserList.js b/static/js/folderBrowserList.js index 81c8ad1b..598cf8bc 100644 --- a/static/js/folderBrowserList.js +++ b/static/js/folderBrowserList.js @@ -7,7 +7,7 @@ pandora.ui.folderBrowserList = function(id, section) { var ui = pandora.user.ui, columnWidth = (ui.sidebarSize - Ox.UI.SCROLLBAR_SIZE - (section != 'texts' ? 96 : 48)) / 2, i = Ox.getIndexById(pandora.site.sectionFolders[section], id), - folderItems = section == 'items' ? 'Lists' : Ox.toTitleCase(section), + folderItems = pandora.getFolderItems(section), folderItem = folderItems.slice(0, -1), that = Ox.TableList({ columns: [ @@ -152,7 +152,12 @@ pandora.ui.folderBrowserList = function(id, section) { // not-featured list may be in the user's favorites folder keys: id == 'featured' ? ['subscribed'] : [], pageLength: 1000, - selected: pandora.getListData().folder == id ? [section == 'items' ? ui._list : ui[section.slice(0, -1)]] : [], + selected: pandora.getListData().folder == id + ? [{ + items: ui._list, + documents: ui._documentlist + }[section] || ui[section.slice(0, -1)]] + : [], sort: [{key: 'name', operator: '+'}], unique: 'id' }) @@ -226,6 +231,15 @@ pandora.ui.folderBrowserList = function(id, section) { operator: '&' } }); + } else if (section == 'documents') { + pandora.UI.set({ + findDocuments: { + conditions: list ? [ + {key: 'list', value: data.ids[0], operator: '=='} + ] : [], + operator: '&' + } + }); } else { pandora.UI.set(section.slice(0, -1), list); } diff --git a/static/js/folderList.js b/static/js/folderList.js index db3d18cb..46cd0faa 100644 --- a/static/js/folderList.js +++ b/static/js/folderList.js @@ -5,7 +5,7 @@ pandora.ui.folderList = function(id, section) { section = section || pandora.user.section; var ui = pandora.user.ui, i = Ox.getIndexById(pandora.site.sectionFolders[section], id), - folderItems = section == 'items' ? 'Lists' : Ox.toTitleCase(section), + folderItems = pandora.getFolderItems(section), folderItem = folderItems.slice(0, -1), canEditFeatured = pandora.site.capabilities['canEditFeatured' + folderItems][pandora.user.level], $placeholder, @@ -20,9 +20,7 @@ pandora.ui.folderList = function(id, section) { }, format: function(value, data) { return $('').attr({ - src: '/' + folderItem.toLowerCase() + '/' - + encodeURIComponent(data.id) + '/icon.jpg?' - + data.modified + src: pandora.getListIcon(section, data.id, '', data.modified) }).css({ width: '14px', height: '14px', @@ -337,7 +335,7 @@ pandora.ui.folderList = function(id, section) { pandora.api['unsubscribeFrom' + folderItem]({ id: data.ids[0] }, function(result) { - Ox.Request.clearCache('findList'); + Ox.Request.clearCache('find' + folderItems); that.reloadList(); }); } else if (id == 'featured' && canEditFeatured) { @@ -346,7 +344,7 @@ pandora.ui.folderList = function(id, section) { id: data.ids[0], status: 'public' }, function(result) { - Ox.Request.clearCache('findList'); + Ox.Request.clearCache('find' + folderItems); // fixme: duplicated if (result.data.user == pandora.user.username || result.data.subscribed) { pandora.$ui.folderList[ @@ -415,6 +413,20 @@ pandora.ui.folderList = function(id, section) { : that.value(list).view : void 0 }); + } else if (section == 'documents') { + pandora.UI.set({ + findDocuments: { + conditions: list ? [ + {key: 'collection', value: list, operator: '=='} + ] : [], + operator: '&' + }, + collectionView: list + ? pandora.user.ui.collections[list] + ? pandora.user.ui.collections[list].view + : that.value(list).view + : void 0 + }); } else { pandora.UI.set(section.slice(0, -1), list); } diff --git a/static/js/folderPlaceholder.js b/static/js/folderPlaceholder.js index 55a3da12..c3a9bbc8 100644 --- a/static/js/folderPlaceholder.js +++ b/static/js/folderPlaceholder.js @@ -6,12 +6,13 @@ pandora.ui.folderPlaceholder = function(id, section) { .css({ height: '14px', padding: '1px 4px', - }); + }), + folderItems = pandora.getFolderItems(section); that.updateText = function(string, isFind) { return that.html( string != 'volumes' ? Ox._('No {0} {1}' + (isFind ? ' found' : ''), - [Ox._(string), Ox._(section == 'items' ? 'lists' : section)]) + [Ox._(string), Ox._(folderItems.toLowerCase())]) : Ox._('No local volumes') ); }; diff --git a/static/js/folders.js b/static/js/folders.js index 9ef8c154..853d03cf 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -10,10 +10,13 @@ pandora.ui.folders = function(section) { pandora.resizeFolders(); } }), - editable = (ui[ - section == 'items' ? '_list' : section.slice(0, -1) - ] || '').split(':')[0] == pandora.user.username, - folderItems = section == 'items' ? 'Lists' : Ox.toTitleCase(section), + editable = (ui[{ + items: '_list', + edits: 'edit', + documents: '_collection', + texts: 'text' + }[section]] || '').split(':')[0] == pandora.user.username, + folderItems = pandora.getFolderItems(section), folderItem = folderItems.slice(0, -1), canEditFeatured = pandora.site.capabilities['canEditFeatured' + folderItems][pandora.user.level], initCounter = 0, @@ -38,21 +41,21 @@ pandora.ui.folders = function(section) { : Ox._('To create and share your own {0}, please sign up or sign in.', [section]) )]; } else { - if (section == 'items') { + if (Ox.contains(pandora.site.listSections, section)) { extras = [ pandora.$ui.personalListsMenu = Ox.MenuButton({ items: [ - { id: 'newlist', title: Ox._('New List'), keyboard: 'control n' }, - { id: 'newlistfromselection', title: Ox._('New List from Selection'), keyboard: 'shift control n', disabled: ui.listSelection.length == 0 }, - { id: 'newsmartlist', title: Ox._('New Smart List'), keyboard: 'alt control n' }, - { id: 'newsmartlistfromresults', title: Ox._('New Smart List from Results'), keyboard: 'shift alt control n' }, + { id: 'newlist', title: Ox._('New {0}', [Ox._(folderItem)]), keyboard: 'control n' }, + { id: 'newlistfromselection', title: Ox._('New {0} from Selection', [Ox._(folderItem)]), keyboard: 'shift control n', disabled: ui.listSelection.length == 0 }, + { id: 'newsmartlist', title: Ox._('New Smart {0}', [Ox._(folderItem)]), keyboard: 'alt control n' }, + { id: 'newsmartlistfromresults', title: Ox._('New Smart {0} from Results', [Ox._(folderItem)]), keyboard: 'shift alt control n' }, {}, - { id: 'duplicatelist', title: Ox._('Duplicate Selected List'), keyboard: 'control d', disabled: !ui._list }, - { id: 'editlist', title: Ox._('Edit Selected List...'), keyboard: 'control e', disabled: !editable }, - { id: 'deletelist', title: Ox._('Delete Selected List...'), keyboard: 'delete', disabled: !editable } + { id: 'duplicatelist', title: Ox._('Duplicate Selected {0}', [Ox._(folderItem)]), keyboard: 'control d', disabled: !ui._list }, + { id: 'editlist', title: Ox._('Edit Selected {0}...', [Ox._(folderItem)]), keyboard: 'control e', disabled: !editable }, + { id: 'deletelist', title: Ox._('Delete Selected {0}...', [Ox._(folderItem)]), keyboard: 'delete', disabled: !editable } ], title: 'edit', - tooltip: Ox._('Manage Personal Lists'), + tooltip: Ox._('Manage Personal ' + folderItems), type: 'image' }) .bindEvent({ @@ -64,7 +67,7 @@ pandora.ui.folders = function(section) { ], data.id)) { pandora.addList(data.id.indexOf('smart') > -1, data.id.indexOf('from') > -1); } else if (data.id == 'duplicatelist') { - pandora.addList(pandora.user.ui._list); + pandora.addList(ui._list); } else if (data.id == 'editlist') { pandora.ui.listDialog().open(); } else if (data.id == 'deletelist') { @@ -222,9 +225,17 @@ pandora.ui.folders = function(section) { pandora.$ui.folderList.featured.options({selected: [listData.id]}); } else { // and nowhere else - pandora.UI.set({ - find: pandora.site.user.ui.find - }); + if (section == 'items') { + pandora.UI.set({ + find: pandora.site.user.ui.find + }); + } else if (section == 'documents') { + pandora.UI.set({ + findDocuments: pandora.site.user.ui.findDocuments + }); + } else { + Ox.print('unknown section', section); + } } } pandora.$ui.folderBrowser.favorite.replaceWith( @@ -280,9 +291,17 @@ pandora.ui.folders = function(section) { pandora.$ui.folderList.favorite.options({selected: [listData.id]}); } else { // and nowhere else - pandora.UI.set({ - find: pandora.site.user.ui.find - }); + if (section == 'items') { + pandora.UI.set({ + find: pandora.site.user.ui.find + }); + } else if (section == 'documents') { + pandora.UI.set({ + findDocuments: pandora.site.user.ui.findDocuments + }); + } else { + Ox.print('unknown section', section); + } } } pandora.$ui.folderBrowser.featured.replaceWith( @@ -336,7 +355,7 @@ pandora.ui.folders = function(section) { }, toggle: function(data) { data.collapsed && pandora.$ui.folderList[folder.id].loseFocus(); - pandora.UI.set('showFolder.items.' + folder.id, !data.collapsed); + pandora.UI.set('showFolder.' + section + '.' + folder.id, !data.collapsed); pandora.resizeFolders(); } }); @@ -378,7 +397,7 @@ pandora.ui.folders = function(section) { }).bindEvent({ click: function() { var $dialog = pandora.ui.iconDialog({ - buttons: title != Ox._('Featured Lists') ? [ + buttons: title != Ox._('Featured ' + folderItems) ? [ Ox.Button({title: Ox._('Sign Up...')}).bindEvent({ click: function() { $dialog.close(); @@ -441,6 +460,17 @@ pandora.ui.folders = function(section) { } */ }, + pandora_finddocuments: function() { + var folder = pandora.getListData().folder, + list = pandora.user.ui._collection, + previousList = pandora.UI.getPrevious()._collection; + if (list != previousList) { + Ox.forEach(pandora.$ui.folderList, function($list, id) { + id != folder && $list.options('selected', []); + }); + folder && pandora.$ui.folderList[folder].options({selected: [list]}); + } + }, pandora_text: function() { if (!pandora.user.ui.text) { Ox.forEach(pandora.$ui.folderList, function($list, id) { diff --git a/static/js/info.js b/static/js/info.js index 22df5ece..07e9e58c 100644 --- a/static/js/info.js +++ b/static/js/info.js @@ -3,7 +3,7 @@ pandora.ui.info = function() { var ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1), view = getView(), @@ -13,6 +13,11 @@ pandora.ui.info = function() { toggle: function(data) { pandora.UI.set({showInfo: !data.collapsed}); }, + pandora_documentlist: function() { + if (pandora.user.ui._collection != pandora.UI.getPrevious('_collection')) { + updateInfo(); + } + }, pandora_edit: updateInfo, pandora_find: function() { if (pandora.user.ui._list != pandora.UI.getPrevious('_list')) { @@ -29,6 +34,8 @@ pandora.ui.info = function() { updateInfo(); } }, + pandora_document: updateInfo, + pandora_collectionselection: updateInfo, pandora_text: updateInfo }); @@ -184,16 +191,14 @@ pandora.ui.info = function() { pandora.ui.listInfo = function() { var ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1), list = pandora.user.ui.section == 'items' ? pandora.user.ui._list : ui[folderItem.toLowerCase()], canEditFeaturedLists = pandora.site.capabilities['canEditFeatured' + folderItems][pandora.user.level], that = Ox.Element().css({padding: '16px', textAlign: 'center'}), $icon = Ox.Element('') .attr({ - src: list - ? '/' + folderItem.toLowerCase() + '/' + encodeURIComponent(list) + '/icon256.jpg?' + Ox.uid() - : '/static/png/icon.png' + src: list ? pandora.getListIcon(ui.section, list, 256) : '/static/png/icon.png' }) .css(getIconCSS()) .appendTo(that), diff --git a/static/js/item.js b/static/js/item.js index 1fa5ee1d..beb5cab6 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -22,7 +22,7 @@ pandora.ui.item = function() { if (result.status.code == 200) { // we want to cache the title in any way, so that after closing // a dialog and getting to this item, the title is correct - var documentTitle = pandora.getDocumentTitle(result.data); + var documentTitle = pandora.getWindowTitle(result.data); document.title = pandora.getPageTitle(document.location.pathname) || documentTitle; } diff --git a/static/js/listDialog.js b/static/js/listDialog.js index e9522bdf..a6aaaee2 100644 --- a/static/js/listDialog.js +++ b/static/js/listDialog.js @@ -15,7 +15,7 @@ pandora.ui.listDialog = function(section) { ), ui = pandora.user.ui, width = getWidth(section), - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(pandora.user.ui.section), folderItem = folderItems.slice(0, -1); Ox.getObjectById(tabs, section).selected = true; @@ -26,7 +26,10 @@ pandora.ui.listDialog = function(section) { } else if (id == 'icon') { return pandora.$ui.listIconPanel = pandora.ui.listIconPanel(listData); } else if (id == 'query') { - return pandora.$ui.filterForm = pandora.ui.filterForm({ + return pandora.$ui.filterForm = (pandora.user.ui.section == 'documents' + ? pandora.ui.documentFilterForm + : pandora.ui.filterForm + )({ mode: 'list', list: listData }) @@ -158,7 +161,7 @@ pandora.ui.listDialog = function(section) { pandora.ui.listGeneralPanel = function(listData) { var that = Ox.Element(), ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1); pandora.api['find' + folderItems]({ query: {conditions: [{key: 'id', value: listData.id, operator: '=='}]}, @@ -171,7 +174,7 @@ pandora.ui.listGeneralPanel = function(listData) { tooltip: Ox._('Doubleclick to edit icon') }) .attr({ - src: pandora.getMediaURL('/' + folderItem.toLowerCase() + '/' + encodeURIComponent(listData.id) + '/icon256.jpg?' + Ox.uid()) + src: pandora.getListIcon(ui.section, listData.id, 256) }) .css({ position: 'absolute', @@ -382,13 +385,16 @@ pandora.ui.listIconPanel = function(listData) { quarters = ['top-left', 'top-right', 'bottom-left', 'bottom-right'], ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1), + $iconPanel = Ox.Element(), $icon = $('') - .attr({src: pandora.getMediaURL('/' + folderItem.toLowerCase() + '/' + encodeURIComponent(listData.id) + '/icon256.jpg?' + Ox.uid())}) + .attr({ + src: pandora.getListIcon(ui.section, listData.id, 256) + }) .css({position: 'absolute', borderRadius: '64px', margin: '16px'}) .appendTo($iconPanel), @@ -399,8 +405,6 @@ pandora.ui.listIconPanel = function(listData) { $list = Ox.Element(), ui = pandora.user.ui, - folderItems = ui.section == 'items' ? 'Lists' : Ox.toTitleCase(ui.section), - folderItem = folderItems.slice(0, -1), that = Ox.SplitPanel({ elements: [ @@ -586,9 +590,7 @@ pandora.ui.listIconPanel = function(listData) { posterFrames: posterFrames }, function() { $icon.attr({ - src: pandora.getMediaURL('/' + folderItem.toLowerCase() - + '/' + encodeURIComponent(listData.id) + '/icon256.jpg?' + Ox.uid() - ) + src: pandora.getListIcon(ui.section, listData.id, 256) }); pandora.$ui.folderList[listData.folder].$element .find('img[src*="' @@ -596,10 +598,7 @@ pandora.ui.listIconPanel = function(listData) { + '/"]' ) .attr({ - src: pandora.getMediaURL('/' + folderItem.toLowerCase() - + '/' + encodeURIComponent(listData.id) - + '/icon.jpg?' + Ox.uid() - ) + src: pandora.getListIcon(ui.section, listData.id, 256) }); pandora.$ui.info.updateListInfo(); }); @@ -619,7 +618,7 @@ pandora.ui.listIconPanel = function(listData) { pandora.api.find(Ox.extend(data, { query: { conditions: ( - ui.section == 'items' + Ox.contains(pandora.site.listSections, ui.section) ? [{key: 'list', value: listData.id, operator: '=='}] : []).concat( value !== '' diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index a7a9b1eb..7dd2efea 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -135,22 +135,22 @@ pandora.ui.mainMenu = function() { }) } ] }, {}, - { + { id: 'showsidebar', title: Ox._((ui.showSidebar ? 'Hide' : 'Show') + ' Sidebar'), keyboard: 'shift s' }, - { + { id: 'showinfo', title: Ox._((ui.showInfo ? 'Hide' : 'Show') + ' Info'), disabled: !ui.showSidebar, keyboard: 'shift i' }, - { + { id: 'showfilters', title: Ox._((ui.showFilters ? 'Hide' : 'Show') + ' Filters'), disabled: ui.section != 'items' || !!ui.item, keyboard: 'shift f' }, - { + { id: 'showbrowser', title: Ox._((ui.showBrowser ? 'Hide': 'Show') + ' {0} Browser', [Ox._(pandora.site.itemName.singular)]), disabled: !ui.item, keyboard: 'shift b' @@ -165,18 +165,18 @@ pandora.ui.mainMenu = function() { title: Ox._((ui.showTimeline ? 'Hide' : 'Show') + ' Timeline'), disabled: !hasTimeline(), keyboard: 'shift t' }, - { + { id: 'showannotations', title: Ox._((ui.showAnnotations ? 'Hide' : 'Show') + ' Annotations'), disabled: !hasAnnotations(), keyboard: 'shift a' }, - { + { id: 'showclips', title: Ox._((ui.showClips ? 'Hide' : 'Show') + ' Clips'), disabled: !hasClips(), keyboard: 'shift c' }, {}, - { + { id: 'togglefullscreen', title: Ox._((fullscreenState ? 'Exit' : 'Enter') + ' Fullscreen'), disabled: fullscreenState === void 0, @@ -185,7 +185,7 @@ pandora.ui.mainMenu = function() { : 'F11' }, - { + { id: 'entervideofullscreen', title: Ox._('Enter Video Fullscreen'), disabled: !ui.item || ui.itemView != 'player' @@ -253,6 +253,14 @@ pandora.ui.mainMenu = function() { } else { that.checkItem('allitems'); } + } else if (ui.section == 'documents') { + if (data.checked) { + pandora.UI.set({ + findDocuments: {conditions: [], operator: '&'} + }); + } else { + that.checkItem('allitems'); + } } else { pandora.UI.set(ui.section.slice(0, -1), ''); } @@ -268,6 +276,10 @@ pandora.ui.mainMenu = function() { } else { pandora.UI.set({itemSort: [{key: value, operator: pandora.getSortOperator(value)}]}); } + } else if (data.id == 'documentorder') { + pandora.UI.set({collectionSort: [{key: ui.collectionSort[0].key, operator: value == 'ascending' ? '+' : '-'}]}); + } else if (data.id == 'documentsort') { + pandora.UI.set({collectionSort: [{key: value, operator: pandora.getDocumentSortOperator(value)}]}); } else if (data.id == 'find') { if (value) { pandora.$ui.findSelect.value(value); @@ -351,6 +363,15 @@ pandora.ui.mainMenu = function() { operator: '&' } }); + } else if (ui.section == 'documents') { + pandora.UI.set({ + findDocuments: { + conditions: data.checked ? [ + {key: 'collection', value: data.id.slice(8).replace(/\t/g, '_'), operator: '=='} + ] : [], + operator: '&' + } + }); } else { pandora.UI.set(ui.section.slice(0, -1), data.id.slice(8).replace(/\t/g, '_')); } @@ -389,7 +410,11 @@ pandora.ui.mainMenu = function() { } else if (data.id == 'editlist') { pandora.ui.listDialog().open(); } else if (data.id == 'add') { - pandora.$ui.addItemDialog = pandora.ui.addItemDialog().open(); + if (ui.section == 'documents') { + pandora.$ui.addDocumentDialog = pandora.ui.addDocumentDialog().open(); + } else { + pandora.$ui.addItemDialog = pandora.ui.addItemDialog().open(); + } } else if (data.id == 'edit') { pandora.ui.editItemDialog().open(); } else if (data.id == 'deletelist') { @@ -454,6 +479,13 @@ pandora.ui.mainMenu = function() { pandora.UI.set({listSelection: items}); pandora.reloadList(); }); + } else if (ui.section == 'documents') { + var items = pandora.clipboard.paste('document'); + items.length && pandora.doHistory('paste', items, ui._collection, function() { + //fixme: + //pandora.UI.set({listSelection: items}); + //pandora.reloadList(); + }); } else if (ui.section == 'edits') { var clips = pandora.clipboard.paste('clip'); clips.length && pandora.doHistory('paste', clips, ui.edit, function(result) { @@ -489,6 +521,26 @@ pandora.ui.mainMenu = function() { }).open(); }); } + } else if (ui.section == 'documents') { + var files; + if (ui.document) { + files = [pandora.$ui.document.info()]; + } else { + files = pandora.$ui.list.options('selected').map(function(id) { + return pandora.$ui.list.value(id); + }); + } + pandora.ui.deleteDocumentDialog( + files, + function() { + Ox.Request.clearCache(); + if (ui.document) { + pandora.UI.set({document: ''}); + } else { + pandora.$ui.list.reloadList() + } + } + ).open(); } else if (ui.section == 'edits') { var clips = pandora.$ui.editPanel.getSelectedClips(); pandora.doHistory('delete', clips, ui.edit, function(result) { @@ -594,6 +646,30 @@ pandora.ui.mainMenu = function() { pandora.$ui.errorlogsDialog = pandora.ui.errorlogsDialog().open(); } }, + pandora_collectionsort: function(data) { + that.checkItem('sortMenu_sortitems_' + data.value[0].key); + that.checkItem('sortMenu_orderitems_' + ( + data.value[0].operator == '+' ? 'ascending' : 'descending') + ); + }, + pandora_finddocuments: function() { + var action = pandora.getListData().editable ? 'enableItem' : 'disableItem', + list = ui._collection, + previousList = pandora.UI.getPrevious()._collection; + if (list != previousList) { + that.uncheckItem(previousList == '' ? 'allitems' : 'viewlist' + previousList.replace(/_/g, Ox.char(9))); + that.checkItem(list == '' ? 'allitems' : 'viewlist' + list.replace(/_/g, '\t')); + } + that[ui._list ? 'enableItem' : 'disableItem']('duplicatelist'); + that[action]('editlist'); + that[action]('deletelist'); + that[ui.listSelection.length ? 'enableItem' : 'disableItem']('newlistfromselection'); + that.replaceMenu('itemMenu', getItemMenu()); + that[ui.find.conditions.length ? 'enableItem' : 'disableItem']('clearquery'); + that[Ox.sum(ui._filterState.map(function(filterState) { + return filterState.selected.length; + })) > 0 ? 'enableItem' : 'disableItem']('clearfilters'); + }, pandora_edit: function() { var action = pandora.getListData().editable ? 'enableItem' : 'disableItem', edit = ui.edit, @@ -690,6 +766,14 @@ pandora.ui.mainMenu = function() { pandora.getItemIdAndPosition() ? 'enableItem' : 'disableItem' ]('findsimilar'); }, + pandora_collectionselection: function(data) { + var action = data.value.length ? 'enableItem' : 'disableItem'; + that[action]('newlistfromselection'); + that.replaceMenu('itemMenu', getItemMenu()); + that[ + pandora.getItemIdAndPosition() ? 'enableItem' : 'disableItem' + ]('findsimilar'); + }, pandora_listselection: function(data) { var action = data.value.length ? 'enableItem' : 'disableItem'; that[action]('newlistfromselection'); @@ -983,6 +1067,156 @@ pandora.ui.mainMenu = function() { elements[Ox.mod((index + direction), elements.length)].gainFocus(); } + function getDocumentMenu() { + var listData = pandora.getListData(), + deleteVerb = ui._collection ? Ox._('Remove') : Ox._('Delete'), + isEditable = listData.editable && listData.type == 'static', + isListView = !ui.document, + listName = ui._collection ? Ox._('from List') : Ox._('from Archive'), + listItemsName = 'Documents', + selectionItems = ui.collectionSelection.length, + selectionItemName = ( + selectionItems > 1 ? Ox.formatNumber(selectionItems) + ' ' : '' + ) + Ox._(selectionItems == 1 ? 'Document' : 'Documents'), + clipboardItems = pandora.clipboard.items('document'), + clipboardItemName = clipboardItems == 0 ? '' + : ( + clipboardItems > 1 ? Ox.formatNumber(clipboardItems) + ' ' : '' + ) + Ox._(clipboardItems == 1 ? 'Document' : 'Documents'), + canEdit = false, //fixme + canDelete = ( + ui.document || ui.collectionSelection.length + ) && ( + pandora.site.capabilities.canRemoveDocuments[pandora.user.level] || + ui.collectionSelection.every(function(item) { + return pandora.$ui.list.value(item, 'editable'); + }) + ), + canSelect = isListView, + canCopy = ui.collectionSelection.length, + canCut = canCopy && isEditable, + canPaste = isListView && isEditable, + canAdd = canCopy && clipboardItems > 0, + historyItems = pandora.history.items(), + undoText = pandora.history.undoText(), + redoText = pandora.history.redoText(); + return { id: 'itemMenu', title: Ox._('Item'), items: [ + { id: 'add', title: Ox._('Add {0}...', [Ox._('Document')]), disabled: !pandora.site.capabilities.canAddItems[pandora.user.level] }, + { id: 'edit', title: Ox._('Edit {0}...', [Ox._('Document')]), disabled: true /*fixme: !canEdit */ }, + {}, + { id: 'selectall', title: Ox._('Select All {0}', [listItemsName]), disabled: !canSelect, keyboard: 'control a' }, + { id: 'selectnone', title: Ox._('Select None'), disabled: !canSelect, keyboard: 'shift control a' }, + { id: 'invertselection', title: Ox._('Invert Selection'), disabled: !canSelect, keyboard: 'alt control a' }, + {}, + { id: 'cut', title: Ox._('Cut {0}', [selectionItemName]), disabled: !canCut, keyboard: 'control x' }, + { id: 'cutadd', title: Ox._('Cut and Add to Clipboard'), disabled: !canCut || !canAdd, keyboard: 'shift control x' }, + { id: 'copy', title: Ox._('Copy {0}', [selectionItemName]), disabled: !canCopy, keyboard: 'control c' }, + { id: 'copyadd', title: Ox._('Copy and Add to Clipboard'), disabled: !canCopy || !canAdd, keyboard: 'shift control c' }, + { id: 'paste', title: clipboardItems == 0 ? Ox._('Paste') : Ox._('Paste {0}', [clipboardItemName]), disabled: !canPaste, keyboard: 'control v' }, + { id: 'clearclipboard', title: Ox._('Clear Clipboard'), disabled: !clipboardItems}, + {}, + { id: 'delete', title: Ox._('{0} {1} {2}', [deleteVerb, selectionItemName, listName]), disabled: !canDelete, keyboard: 'delete' }, + {}, + { id: 'undo', title: undoText ? Ox._('Undo {0}', [undoText]) : Ox._('Undo'), disabled: !undoText, keyboard: 'control z' }, + { id: 'redo', title: redoText ? Ox._('Redo {0}', [redoText]) : Ox._('Redo'), disabled: !redoText, keyboard: 'shift control z' }, + { id: 'clearhistory', title: Ox._('Clear History'), disabled: !historyItems } + ] }; + + } + + function getCollectionMenu() { + var itemNamePlural = pandora.getFolderItems(ui.section), + itemNameSingular = itemNamePlural.slice(0, -1), + disableEdit = isGuest || !ui._collection, + disableFromSelection = isGuest || ui.collectionSelection.length == 0; + + return { id: 'listMenu', title: Ox._(itemNameSingular == 'Collection' ? 'File' : itemNameSingular), items: [].concat( + { + id: 'allitems', + title: pandora.getAllItemsTitle(), + checked: !ui._collection, + keyboard: 'shift control w' + }, + ['personal', 'favorite', 'featured'].map(function(folder) { + return { + id: folder + 'lists', + title: Ox._(Ox.toTitleCase(folder) + ' ' + itemNamePlural), + items: Ox.isUndefined(lists[folder]) + ? [{id: 'loading', title: Ox._('Loading...'), disabled: true}] + : lists[folder].length == 0 + ? [{id: 'nolists', title: Ox._('No {0} {1}', + [Ox._(Ox.toTitleCase(folder)), Ox._(itemNamePlural)]), disabled: true}] + : lists[folder].map(function(list) { + return { + id: 'viewlist' + list.id.replace(/_/g, Ox.char(9)), + title: Ox.encodeHTMLEntities(( + folder == 'favorite' ? list.user + ': ' : '' + ) + list.name), + checked: list.id == ui._collection + }; + }) + }; + }), + [ + {}, + { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, + { id: 'newlistfromselection', title: Ox._('New ' + itemNameSingular + ' from Selection'), disabled: disableFromSelection, keyboard: 'shift control n' }, + { id: 'newsmartlist', title: Ox._('New Smart ' + itemNameSingular), disabled: isGuest, keyboard: 'alt control n' }, + { id: 'newsmartlistfromresults', title: Ox._('New Smart ' + itemNameSingular + ' from Results'), disabled: isGuest, keyboard: 'shift alt control n' }, + {}, + { id: 'duplicatelist', title: Ox._('Duplicate Selected ' + itemNameSingular), disabled: disableEdit, keyboard: 'control d' }, + { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, + { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' }, + {}, + { id: 'print', title: Ox._('Print'), keyboard: 'control p' } + ] + )}; + }; + + function getEditMenu() { + var itemNameSingular = 'Edit', + itemNamePlural = 'Edits', + disableEdit = isGuest || !ui.edit; + return { id: 'listMenu', title: Ox._(itemNameSingular), items: [].concat( + { + id: 'allitems', + title: pandora.getAllItemsTitle(), + checked: !ui.edit, + keyboard: 'shift control w' + }, + ['personal', 'favorite', 'featured'].map(function(folder) { + return { + id: folder + 'lists', + title: Ox._(Ox.toTitleCase(folder) + ' ' + itemNamePlural), + items: Ox.isUndefined(lists[folder]) + ? [{id: 'loading', title: Ox._('Loading...'), disabled: true}] + : lists[folder].length == 0 + ? [{id: 'nolists', title: Ox._('No {0} {1}', + [Ox._(Ox.toTitleCase(folder)), Ox._(itemNamePlural)]), disabled: true}] + : lists[folder].map(function(list) { + return { + id: 'viewlist' + list.id.replace(/_/g, Ox.char(9)), + title: Ox.encodeHTMLEntities(( + folder == 'favorite' ? list.user + ': ' : '' + ) + list.name), + checked: list.id == ui.edit + }; + }) + }; + }), + [ + {}, + { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, + { id: 'newlistfromselection', title: Ox._('New ' + itemNameSingular + ' from Selection'), disabled: disableEdit, keyboard: 'shift control n' }, + { id: 'newsmartlist', title: Ox._('New Smart ' + itemNameSingular), disabled: isGuest, keyboard: 'alt control n' }, + {}, + { id: 'duplicatelist', title: Ox._('Duplicate Selected ' + itemNameSingular), disabled: disableEdit, keyboard: 'control d' }, + { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, + { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' } + ] + )}; + } + function getFindMenu() { return { id: 'findMenu', title: Ox._('Find'), items: [ { id: 'find', title: Ox._('Find'), items: [ @@ -1005,7 +1239,62 @@ pandora.ui.mainMenu = function() { ] }; } + function getItemListMenu() { + var itemNameSingular = 'List', + itemNamePlural = 'Lists', + disableEdit = isGuest || !ui._list, + disableFromSelection = isGuest || ui.listSelection.length == 0; + + return { id: 'listMenu', title: Ox._(itemNameSingular), items: [].concat( + { + id: 'allitems', + title: pandora.getAllItemsTitle(), + checked: !ui._list, + keyboard: 'shift control w' + }, + ['personal', 'favorite', 'featured'].map(function(folder) { + return { + id: folder + 'lists', + title: Ox._(Ox.toTitleCase(folder) + ' ' + itemNamePlural), + items: Ox.isUndefined(lists[folder]) + ? [{id: 'loading', title: Ox._('Loading...'), disabled: true}] + : lists[folder].length == 0 + ? [{id: 'nolists', title: Ox._('No {0} {1}', + [Ox._(Ox.toTitleCase(folder)), Ox._(itemNamePlural)]), disabled: true}] + : lists[folder].map(function(list) { + return { + id: 'viewlist' + list.id.replace(/_/g, Ox.char(9)), + title: Ox.encodeHTMLEntities(( + folder == 'favorite' ? list.user + ': ' : '' + ) + list.name), + checked: list.id == ui._list + }; + }) + }; + }), + [ + {}, + { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, + { id: 'newlistfromselection', title: Ox._('New ' + itemNameSingular + ' from Selection'), disabled: disableFromSelection, keyboard: 'shift control n' }, + { id: 'newsmartlist', title: Ox._('New Smart ' + itemNameSingular), disabled: isGuest, keyboard: 'alt control n' }, + { id: 'newsmartlistfromresults', title: Ox._('New Smart ' + itemNameSingular + ' from Results'), disabled: isGuest, keyboard: 'shift alt control n' }, + { id: 'neweditfromselection', title: Ox._('New Edit from Selection'), disabled: disableFromSelection }, + { id: 'newsmarteditfromresults', title: Ox._('New Smart Edit from Results'), disabled: isGuest }, + {}, + { id: 'duplicatelist', title: Ox._('Duplicate Selected ' + itemNameSingular), disabled: disableEdit, keyboard: 'control d' }, + { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, + { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' }, + {}, + { id: 'print', title: Ox._('Print'), keyboard: 'control p' }, + { id: 'tv', title: Ox._('TV'), keyboard: 'control space' } + ] + )}; + }; + function getItemMenu() { + if (ui.section == 'documents') { + return getDocumentMenu(); + } var listData = pandora.getListData(), deleteVerb = ui._list ? Ox._('Remove') : Ox._('Delete'), isEditable = listData.editable && listData.type == 'static', @@ -1021,7 +1310,7 @@ pandora.ui.mainMenu = function() { && ui.editView != 'annotations', // FIXME: focus listName = isVideoView || isClipView ? '' : ui.section == 'items' ? ( - ui._list ? Ox._('from List') : Ox._('from Archive') + ui._? Ox._('from List') : Ox._('from Archive') ) : Ox._('from Edit'), listItemsName = Ox._( @@ -1057,15 +1346,18 @@ pandora.ui.mainMenu = function() { ) ) && pandora.$ui.list.value(ui.listSelection[0], 'editable') ), - canDelete = pandora.site.capabilities.canRemoveItems[pandora.user.level] || ( + canDelete = ( ui.section == 'items' && ( ui.item || ( Ox.contains(['list', 'grid', 'clips', 'timelines'], ui.listView) && ui.listSelection.length ) - ) && ui.listSelection.every(function(item) { - return pandora.$ui.list.value(item, 'editable'); - }) + ) && ( + pandora.site.capabilities.canRemoveItems[pandora.user.level] || + ui.listSelection.every(function(item) { + return pandora.$ui.list.value(item, 'editable'); + }) + ) ), canSelect = isListView || isClipView || isEditView, canCopy = isListView ? ui.listSelection.length @@ -1106,15 +1398,22 @@ pandora.ui.mainMenu = function() { } function getListMenu() { - var itemNameSingular = ui.section == 'items' ? 'List' : ui.section == 'edits' ? 'Edit' : 'Text', - itemNamePlural = ui.section == 'items' ? 'Lists' : ui.section == 'edits' ? 'Edits' : 'Texts'; + return ({ + items: getItemListMenu, + documents: getCollectionMenu, + edits: getEditMenu, + texts: getTextMenu + }[ui.section])(); + } + + function getTextMenu() { + var itemNameSingular = 'Text', + itemNamePlural = 'Texts'; return { id: 'listMenu', title: Ox._(itemNameSingular), items: [].concat( { id: 'allitems', title: pandora.getAllItemsTitle(), - checked: ui.section == 'items' ? !ui.item && !ui._list - : ui.section == 'edits' ? !ui.edit - : !ui.text, + checked: !ui.text, keyboard: 'shift control w' }, ['personal', 'favorite', 'featured'].map(function(folder) { @@ -1132,9 +1431,7 @@ pandora.ui.mainMenu = function() { title: Ox.encodeHTMLEntities(( folder == 'favorite' ? list.user + ': ' : '' ) + list.name), - checked: ui.section == 'items' ? list.id == ui._list - : ui.section == 'edits' ? list.id == ui.edit - : list.id == ui.text + checked: list.id == ui.text }; }) }; @@ -1142,38 +1439,47 @@ pandora.ui.mainMenu = function() { [ {}, { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, - ], - ui.section == 'items' ? [ - { id: 'newlistfromselection', title: Ox._('New ' + itemNameSingular + ' from Selection'), disabled: isGuest || ui.listSelection.length == 0, keyboard: 'shift control n' }, - { id: 'newsmartlist', title: Ox._('New Smart ' + itemNameSingular), disabled: isGuest, keyboard: 'alt control n' }, - { id: 'newsmartlistfromresults', title: Ox._('New Smart ' + itemNameSingular + ' from Results'), disabled: isGuest, keyboard: 'shift alt control n' }, - { id: 'neweditfromselection', title: Ox._('New Edit from Selection'), disabled: isGuest || ui.listSelection.length == 0 }, - { id: 'newsmarteditfromresults', title: Ox._('New Smart Edit from Results'), disabled: isGuest } - ] : ui.section == 'edits' ? [ - { id: 'newlistfromselection', title: Ox._('New ' + itemNameSingular + ' from Selection'), disabled: isGuest || !ui.edit, keyboard: 'shift control n' }, - { id: 'newsmartlist', title: Ox._('New Smart ' + itemNameSingular), disabled: isGuest, keyboard: 'alt control n' } - ] : [ { id: 'newpdf', title: Ox._('New PDF'), disabled: isGuest, keyboard: 'alt control n' }, - ], - [ - {} - ], - ui.section != 'texts' ? [ - { id: 'duplicatelist', title: Ox._('Duplicate Selected ' + itemNameSingular), disabled: isGuest || (ui.section == 'items' && !ui._list) || (ui.section == 'edits' && !ui.edit), keyboard: 'control d' } - ] : [], - [ - { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: isGuest || (ui.section == 'items' && !ui._list) || (ui.section == 'edits' && !ui.edit), keyboard: 'control e' }, - { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: isGuest || (ui.section == 'items' && !ui._list) || (ui.section == 'edits' && !ui.edit), keyboard: 'delete' } - ], - ui.section == 'items' ? [ {}, - { id: 'print', title: Ox._('Print'), keyboard: 'control p' }, - { id: 'tv', title: Ox._('TV'), keyboard: 'control space' } - ] : [] + { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: isGuest, keyboard: 'control e' }, + { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: isGuest, keyboard: 'delete' } + ] )}; - }; + } + + function getCollectionSortMenu() { + var isClipView = false, + clipItems = [].concat(!ui.document ? pandora.site.documentSortKeys.map(function(key) { + return Ox.extend({ + checked: ui.collectionSort[0].key == key.id + }, key); + }) : []); + return { id: 'sortMenu', title: Ox._('Sort'), items: [ + { id: 'sortitems', title: Ox._('Sort {0} by', [Ox._('Documents')]), disabled: ui.document, items: [ + { group: 'documentsort', min: 1, max: 1, items: pandora.site.documentSortKeys.map(function(key) { + return Ox.extend({ + checked: ui.collectionSort[0].key == key.id + }, key, { + title: Ox._(key.title) + }); + }) } + ] }, + { id: 'orderitems', title: Ox._('Order {0}', [Ox._('Documents')]), disabled: ui.document, items: [ + { group: 'documentorder', min: 1, max: 1, items: [ + { id: 'ascending', title: Ox._('Ascending'), checked: (ui.collectionSort[0].operator || pandora.getSortOperator(ui.collectionSort[0].key)) == '+' }, + { id: 'descending', title: Ox._('Descending'), checked: (ui.collectionSort[0].operator || pandora.getSortOperator(ui.collectionSort[0].key)) == '-' } + ]} + ] }, + { id: 'advancedsort', title: Ox._('Advanced Sort...'), keyboard: 'shift control s', disabled: true }, + ] }; + } function getSortMenu() { + + if (ui.section == 'documents') { + return getCollectionSortMenu(); + } + //fixme split items/clips menu var isClipView = pandora.isClipView(), clipItems = (isClipView ? pandora.site.clipKeys.map(function(key) { return Ox.extend(Ox.clone(key), { @@ -1211,10 +1517,10 @@ pandora.ui.mainMenu = function() { title: Ox._('Ascending'), checked: (ui.listSort[0].operator || pandora.getSortOperator(ui.listSort[0].key)) == '+' }, - { + { id: 'descending', title: Ox._('Descending'), - checked: (ui.listSort[0].operator || pandora.getSortOperator(ui.listSort[0].key)) == '-' + checked: (ui.listSort[0].operator || pandora.getSortOperator(ui.listSort[0].key)) == '-' } ]} ] }, diff --git a/static/js/mainPanel.js b/static/js/mainPanel.js index 8bf851ad..333fe789 100644 --- a/static/js/mainPanel.js +++ b/static/js/mainPanel.js @@ -21,6 +21,17 @@ pandora.ui.mainPanel = function() { orientation: 'horizontal' }) .bindEvent({ + pandora_finddocuments: function() { + var previousUI = pandora.UI.getPrevious(); + if (!previousUI.document && ui._list == previousUI._list) { + that.replaceElement(1, pandora.$ui.documentPanel = pandora.ui.documentPanel()); + } + }, + pandora_document: function(data) { + if (!data.value || !data.previousValue) { + that.replaceElement(1, pandora.$ui.documentPanel = pandora.ui.documentPanel()); + } + }, pandora_edit: function(data) { that.replaceElement(1, pandora.$ui.editPanel = pandora.ui.editPanel()); }, @@ -92,6 +103,7 @@ pandora.ui.mainPanel = function() { function getRightPanel() { return ui.section == 'items' ? pandora.$ui.rightPanel = pandora.ui.rightPanel() : ui.section == 'edits' ? pandora.$ui.editPanel = pandora.ui.editPanel() + : ui.section == 'documents' ? pandora.$ui.documentPanel = pandora.ui.documentPanel() : pandora.$ui.textPanel = pandora.ui.textPanel(); } return that; diff --git a/static/js/pandora.js b/static/js/pandora.js index aeb44c9a..a8ae8842 100644 --- a/static/js/pandora.js +++ b/static/js/pandora.js @@ -351,7 +351,11 @@ appPanel findKeys: data.site.itemKeys.filter(function(key) { return key.find; }), + documentFindKeys: data.site.documentKeys.filter(function(key) { + return key.find; + }), itemsSection: pandora.site.itemName.plural.toLowerCase(), + listSections: ['items', 'documents'], map: data.site.layers.some(function(layer) { return layer.type == 'place' }) ? 'manual' : data.site.layers.some(function(layer) { @@ -364,6 +368,11 @@ appPanel {id: 'featured', title: 'Featured Lists', showBrowser: false}, {id: 'volumes', title: 'Local Volumes'} ], + documents: [ + {id: 'personal', title: 'Personal Collections'}, + {id: 'favorite', title: 'Favorite Collections', showBrowser: false}, + {id: 'featured', title: 'Featured Collections', showBrowser: false} + ], edits: [ {id: 'personal', title: 'Personal Edits'}, {id: 'favorite', title: 'Favorite Edits', showBrowser: false}, @@ -375,7 +384,12 @@ appPanel {id: 'featured', title: 'Featured Texts', showBrowser: false} ] }, - sortKeys: pandora.getSortKeys() + sortKeys: pandora.getSortKeys(), + documentSortKeys: pandora.getDocumentSortKeys(), + collectionViews: [ + {id: 'list', title: Ox._('View as List')}, + {id: 'grid', title: Ox._('View as Grid')} + ] }); pandora.site.listSettings = {}; Ox.forEach(pandora.site.user.ui, function(val, key) { @@ -383,6 +397,12 @@ appPanel pandora.site.listSettings[key] = key[4].toLowerCase()+ key.slice(5); } }); + pandora.site.collectionSettings = {}; + Ox.forEach(pandora.site.user.ui, function(val, key) { + if (/^collection[A-Z]/.test(key)) { + pandora.site.collectionSettings[key] = key[10].toLowerCase()+ key.slice(11); + } + }); pandora.site.editSettings = { clip: '', 'in': 0, diff --git a/static/js/sectionButtons.js b/static/js/sectionButtons.js index 568ece23..73ee40e0 100644 --- a/static/js/sectionButtons.js +++ b/static/js/sectionButtons.js @@ -5,7 +5,7 @@ pandora.ui.sectionButtons = function(section) { buttons: [ {id: 'items', title: Ox._(pandora.site.itemName.plural)}, {id: 'edits', title: Ox._('Edits')}, - {id: 'texts', title: Ox._('Texts')} + {id: 'documents', title: Ox._('Documents')} ], id: 'sectionButtons', selectable: true, diff --git a/static/js/sectionSelect.js b/static/js/sectionSelect.js index 3329b66e..7018acfe 100644 --- a/static/js/sectionSelect.js +++ b/static/js/sectionSelect.js @@ -7,7 +7,7 @@ pandora.ui.sectionSelect = function(section) { items: [ {id: 'items', title: Ox._(pandora.site.itemName.plural)}, {id: 'edits', title: Ox._('Edits')}, - {id: 'texts', title: Ox._('Texts')} + {id: 'documents', title: Ox._('Documents')} ], value: section || pandora.user.ui.section }).css({ diff --git a/static/js/textPanel.js b/static/js/textPanel.js index 538cde2e..ee702a4e 100644 --- a/static/js/textPanel.js +++ b/static/js/textPanel.js @@ -11,6 +11,7 @@ pandora.ui.textPanel = function() { orientation: 'vertical' }), embedURLs, + scrolling = false, selected = -1, selectedURL; @@ -233,12 +234,23 @@ pandora.ui.textHTML = function(text) { scroll: function(event) { var position = Math.round(100 * that[0]. scrollTop / Math.max(1, that[0].scrollHeight - that.height())), + settings; + if (pandora.user.ui.section == 'texts') { settings = pandora.user.ui.texts[pandora.user.ui.text]; + } else { + settings = pandora.user.ui.documents[pandora.user.ui.document] || {}; + } position = position - position % 10; if (!scrolling && settings && (settings.name || (position != settings.position))) { - pandora.UI.set('texts.' + pandora.UI.encode(pandora.user.ui.text), { - position: position ? position : 0 - }); + if (pandora.user.ui.section == 'documents') { + pandora.UI.set('documents.' + pandora.user.ui.document, { + position: position ? position : 0 + }); + } else { + pandora.UI.set('texts.' + pandora.UI.encode(pandora.user.ui.text), { + position: position ? position : 0 + }); + } } scrolling = false; }, @@ -248,6 +260,9 @@ pandora.ui.textHTML = function(text) { that.update(); }, }) + .bindEvent('pandora_documents.' + text.id.toLowerCase(), function(data) { + data.value && data.value.name && scrollToPosition(); + }) .bindEvent('pandora_texts.' + text.id.toLowerCase(), function(data) { data.value && data.value.name && scrollToPosition(); }), @@ -257,10 +272,10 @@ pandora.ui.textHTML = function(text) { .appendTo(that), $title = Ox.EditableContent({ - editable: text.name ? text.editable : false, + editable: text.title ? text.editable : false, placeholder: text.editable ? Ox._('Doubleclick to edit title') : Ox._('Untitled'), tooltip: text.editable ? pandora.getEditTooltip('title') : '', - value: text.name || Ox._('{0} Texts', [pandora.site.site.name]), + value: text.title || Ox._('{0} Texts', [pandora.site.site.name]), width: width }) .css({ @@ -271,17 +286,31 @@ pandora.ui.textHTML = function(text) { }) .bindEvent({ submit: function(data) { - Ox.Request.clearCache('getText'); - pandora.api.editText({ - id: pandora.user.ui.text, - name: data.value - }, function(result) { - if (result.data.id != pandora.user.ui.text) { - Ox.Request.clearCache(); - pandora.renameList(pandora.user.ui.text, result.data.id, result.data.name); - pandora.$ui.info.updateListInfo(); - } - }); + if (pandora.user.ui.section == 'documents') { + pandora.api.editDocument({ + id: pandora.user.ui.document, + name: data.value + }, function(result) { + if (result.data.name != data.value) { + Ox.Request.clearCache(); + $title.options({ + value: result.data.title + }) + } + }); + } else { + Ox.Request.clearCache('getText'); + pandora.api.editText({ + id: pandora.user.ui.text, + name: data.value + }, function(result) { + if (result.data.id != pandora.user.ui.text) { + Ox.Request.clearCache(); + pandora.renameList(pandora.user.ui.text, result.data.id, result.data.name); + pandora.$ui.info.updateListInfo(); + } + }); + } } }) .appendTo($content), @@ -373,12 +402,23 @@ pandora.ui.textHTML = function(text) { }) .bindEvent({ submit: function(data) { - Ox.Request.clearCache('getText'); - pandora.api.editText({ - id: pandora.user.ui.text, - text: data.value - }); - pandora.$ui.textPanel.update(data.value); + if (pandora.user.ui.section == 'documents') { + Ox.Request.clearCache('getDocument'); + pandora.api.editDocument({ + id: pandora.user.ui.document, + text: data.value + }, function(result) { + //fixme: just reload as it was done with textPanel + pandora.$ui.document = pandora.ui.document(); + }); + } else { + Ox.Request.clearCache('getText'); + pandora.api.editText({ + id: pandora.user.ui.text, + text: data.value + }); + pandora.$ui.textPanel.update(data.value); + } } }) .appendTo($content); @@ -404,7 +444,9 @@ pandora.ui.textHTML = function(text) { } function scrollToPosition() { - var settings = pandora.user.ui.texts[pandora.user.ui.text] || {}, + var settings = (pandora.user.ui.section == 'documents' + ? pandora.user.ui.documents[pandora.user.ui.document] + : pandora.user.ui.texts[pandora.user.ui.text]) || {}, position = settings.position || 0, element, scrollTop; @@ -505,6 +547,7 @@ pandora.ui.textEmbed = function() { resize: function(data) { pandora.user.ui.embedSize = data.size; pandora.$ui.text.update(); + pandora.$ui.document && pandora.$ui.document.update(); }, resizeend: function(data) { $iframe.attr('src') && $overlay.hide(); diff --git a/static/js/uploadDocumentDialog.js b/static/js/uploadDocumentDialog.js index 21423418..42d698a5 100644 --- a/static/js/uploadDocumentDialog.js +++ b/static/js/uploadDocumentDialog.js @@ -46,7 +46,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { if (title == Ox._('Cancel Upload')) { upload && upload.abort(); } else if (title == Ox._('Done')) { - callback({ + callback && callback({ ids: ids }); } @@ -81,7 +81,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { + (extension == 'jpeg' ? 'jpg' : extension); valid && Ox.oshash(file, function(oshash) { pandora.api.findDocuments({ - keys: ['id', 'user', 'name', 'extension'], + keys: ['id', 'user', 'title', 'extension'], query: { conditions: [{ key: 'oshash', @@ -91,10 +91,10 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { operator: '&' }, range: [0, 1], - sort: [{key: 'name', operator: '+'}] + sort: [{key: 'title', operator: '+'}] }, function(result) { if (result.data.items.length) { - var id = result.data.items[0].name + '.' + var id = result.data.items[0].title + '.' + result.data.items[0].extension; valid && errorDialog( filename == id @@ -161,7 +161,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { ids.push(data.response.id); if (part == files.length) { $progress.options({progress: data.progress}); - callback({ids: ids}); + callback && callback({ids: ids}); $uploadDialog.close(); } else { uploadFile(part); diff --git a/static/js/utils.js b/static/js/utils.js index e9a30610..98722721 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -40,6 +40,9 @@ pandora.addFolderItem = function(section) { if (!isSmart) { if (isItems) { data.items = ui.listSelection; + } else if (section == 'documents') { + //fixme + data.items = ui.collectionSelection; } else { data.clips = pandora.getClipData( ui.section == 'items' @@ -66,7 +69,11 @@ pandora.addFolderItem = function(section) { if (data.type == 'smart') { data.query = listData.query; } - pandora.api[isItems ? 'findLists' : 'findEdits']({ + pandora.api[{ + items: 'findLists', + documents: 'findCollections', + edits: 'findEdits', + }[section]]({ query: {conditions: [{ key: 'id', operator: '==', @@ -103,6 +110,9 @@ pandora.addFolderItem = function(section) { addList(); } }); + } else if(section == 'documents') { + //fixme + addList(); } else { pandora.api.getEdit({ id: list, @@ -127,7 +137,11 @@ pandora.addFolderItem = function(section) { }); } function addList() { - pandora.api[isItems ? 'addList' : 'addEdit'](data, function(result) { + pandora.api[{ + items: 'addList', + documents: 'addCollection', + edits: 'addEdit' + }[section]](data, function(result) { getPosterFrames(result.data.id); }); } @@ -136,23 +150,41 @@ pandora.addFolderItem = function(section) { sortKey = Ox.getObjectById(pandora.site.itemKeys, 'votes') ? 'votes' : 'timesaccessed'; if (!isDuplicate) { - (isItems ? Ox.noop : pandora.api.getEdit)({ + ({ + items: Ox.noop, + documents: Ox.noop, + edits: pandora.api.getEdit + }[section])({ id: newList, keys: ['clips'] }, function(result) { - query = isItems ? { - conditions: [{key: 'list', value: newList, operator: '=='}], - operator: '&' - } : { - conditions: Ox.unique(result.data.clips.map(function(clip) { - return {key: 'id', value: clip.item, operator: '=='}; - })), - operator: '|' - }; - (isItems ? pandora.api.find : Ox.noop)({ + if (Ox.contains(pandora.site.listSections, section)) { + query = { + conditions: [{ + key: section == 'documents' ? 'collection' : 'list', + value: newList, operator: '==' + }], + operator: '&' + }; + } else{ + query = { + conditions: Ox.unique(result.data.clips.map(function(clip) { + return {key: 'id', value: clip.item, operator: '=='}; + })), + operator: '|' + }; + } + ({ + items: pandora.api.find, + documents: pandora.api.findDocuments, + edits: Ox.noop + }[section])({ query: { conditions: [ - {key: 'list', value: newList, operator: '=='} + { + key: section == 'documents' ? 'collection' : 'list', + value: newList, operator: '==' + } ], operator: '&' }, @@ -181,7 +213,11 @@ pandora.addFolderItem = function(section) { }); }); } else { - pandora.api[isItems ? 'findLists' : 'findEdits']({ + pandora.api[{ + items: 'findLists', + documents: 'findCollections', + edits: 'findEdits' + }[section]]({ query: { conditions: [{key: 'id', value: list, operator: '=='}], operator: '&' @@ -193,7 +229,11 @@ pandora.addFolderItem = function(section) { } } function setPosterFrames(newList, posterFrames) { - pandora.api[isItems ? 'editList' : 'editEdit']({ + pandora.api[{ + items: 'editList', + documents: 'editCollection', + edits: 'editEdit' + }[section]]({ id: newList, posterFrames: posterFrames }, function() { @@ -206,22 +246,37 @@ pandora.addFolderItem = function(section) { // (same applies to addText, below) $folderList = pandora.$ui.folderList.personal; pandora.$ui.folder[0].options({collapsed: false}); - Ox.Request.clearCache(isItems ? 'findLists' : 'findEdits'); + Ox.Request.clearCache({ + items: 'findLists', + documents: 'findCollections', + edits: 'findEdits' + }[section]); $folderList.bindEventOnce({ load: function() { $folderList.gainFocus() .options({selected: [newList]}) .editCell(newList, 'name', true); - pandora.UI.set(isItems ? { - find: { - conditions: [ - {key: 'list', value: newList, operator: '=='} - ], - operator: '&' + pandora.UI.set({ + items: { + find: { + conditions: [ + {key: 'list', value: newList, operator: '=='} + ], + operator: '&' + } + }, + documents: { + findDocuments: { + conditions: [ + {key: 'collection', value: newList, operator: '=='} + ], + operator: '&' + } + }, + edits: { + edit: newList } - } : { - edit: newList - }); + }[section]); } }).reloadList(); } @@ -229,7 +284,7 @@ pandora.addFolderItem = function(section) { pandora.addList = function() { // addList(isSmart, isFrom) or addList(list) [=duplicate] - pandora.addFolderItem.apply(null, ['items'].concat(Ox.slice(arguments))); + pandora.addFolderItem.apply(null, [pandora.user.ui.section].concat(Ox.slice(arguments))); }; pandora.addText = function(options) { @@ -271,8 +326,7 @@ pandora.beforeUnloadWindow = function() { pandora.changeFolderItemStatus = function(id, status, callback) { var ui = pandora.user.ui, - folderItems = ui.section == 'items' - ? 'Lists' : Ox.toTitleCase(ui.section), + folderItems = pandora.getFolderItems(ui.section), folderItem = folderItems.slice(0, -1); if (status == 'private') { pandora.api['find' + folderItems]({ @@ -491,7 +545,31 @@ pandora.createLinks = function($element) { list: target }, function(result) { callback(result, addedItems); - }); + }); + } else { + callback(null, []); + } + }); + } else if (type == 'documents') { + //fixme + pandora.api.findDocuments({ + query: { + conditions: [{key: 'collection', operator: '==', value: target}], + operator: '&' + }, + positions: items + }, function(result) { + var existingItems = Object.keys(result.data.positions), + addedItems = items.filter(function(item) { + return !Ox.contains(existingItems, item); + }); + if (addedItems.length) { + pandora.api.addCollectionItems({ + items: addedItems, + collection: target + }, function(result) { + callback(result, addedItems); + }); } else { callback(null, []); } @@ -540,6 +618,31 @@ pandora.createLinks = function($element) { // FIXME: Why is this timeout needed? setTimeout(pandora.reloadList, 250); } + } else if (type == 'document' && ui.section == 'documents') { + Ox.Request.clearCache('findDocuments'); + object.targets.filter(function(list) { + return list != ui._list; + }).forEach(function(list) { + listData = pandora.getListData(list); + pandora.api.findDocuments({ + query: { + conditions: [{ + key: 'collection', + operator: '==', + value: list + }], + operator: '&' + } + }, function(result) { + pandora.$ui.folderList[listData.folder].value( + list, 'items', result.data.items + ); + }); + }); + if (Ox.contains(object.targets, ui._list)) { + // FIXME: Why is this timeout needed? + setTimeout(pandora.reloadList, 250); + } } else if (type == 'clip' && ui.section == 'edits') { // FIXME: update edit list (once it has item count) if (Ox.contains(object.targets, ui.edit)) { @@ -743,6 +846,36 @@ pandora.enableDragAndDrop = function($list, canMove, section, getItems) { }); drag.action == 'move' && pandora.reloadList(); }); + } else if (section == 'documents') { + var targets = drag.action == 'copy' ? drag.target.id + : [pandora.user.ui._collection, drag.target.id]; + //fixme use history + //pandora.doHistory(drag.action, drag.ids, targets, function() { + pandora.api.addCollectionItems({ + collection: drag.target.id, + items: drag.ids + + }, function() { + Ox.Request.clearCache('find'); + pandora.api.findDocuments({ + query: { + conditions: [{ + key: 'collection', + operator: '==', + value: drag.target.id + }], + operator: '&' + } + }, function(result) { + var folder = drag.target.status != 'featured' + ? 'personal' : 'featured'; + pandora.$ui.folderList[folder].value( + drag.target.id, 'items', result.data.items + ); + cleanup(250); + }); + drag.action == 'move' && pandora.reloadList(); + }); } else if (section == 'edits') { var targets = drag.action == 'copy' ? drag.target.id : [pandora.user.ui.edit, drag.target.id]; @@ -751,7 +884,10 @@ pandora.enableDragAndDrop = function($list, canMove, section, getItems) { pandora.$ui.editPanel && pandora.$ui.editPanel.updatePanel(); cleanup(250); }); - } + } else { + Ox.print('no drop support for', section); + cleanup(250); + } } } else { cleanup(0); @@ -781,13 +917,20 @@ pandora.enableDragAndDrop = function($list, canMove, section, getItems) { itemName = section == 'items' ? { plural: Ox._(pandora.site.itemName.plural.toLowerCase()), singular: Ox._(pandora.site.itemName.singular.toLowerCase()) - } : { + } : section == 'documents' ? { + plural: Ox._('Documents'), + singular: Ox._('Document') + + } :{ plural: Ox._('clips'), singular: Ox._('clip') }, targetName = section == 'items' ? { plural: Ox._('lists'), singular: Ox._('list') + } : section == 'documents' ? { + plural: Ox._('collections'), + singular: Ox._('collection') } : { plural: Ox._('edits'), singular: Ox._('edit') @@ -946,11 +1089,36 @@ pandora.exitFullscreen = function() { } }; +pandora.formatDocumentKey = function(key, data) { + var value; + if (key.format) { + value = ( + /^color/.test(key.format.type.toLowerCase()) ? Ox.Theme : Ox + )['format' + Ox.toTitleCase(key.format.type)].apply( + this, [data[key.id]].concat(key.format.args || []) + ); + if (key.id == 'rightslevel') { + value.css({width: size * 0.75 + 'px'}); + } + } else { + value = data[key.id]; + if (key.id == 'extension') { + value = value.toUpperCase(); + } else if (key.id == 'dimensions') { + value = Ox.isArray(value) + ? Ox.formatDimensions(value, 'px') + : Ox.formatCount(value, data.extension == 'html' ? 'word' : 'page'); + } + } + return value; +} + pandora.getAllItemsTitle = function(section) { section = section || pandora.user.ui.section; - return section == 'items' - ? Ox._('All {0}', [Ox._(pandora.site.itemName.plural)]) - : Ox._('{0} ' + Ox.toTitleCase(section), [pandora.site.site.name]); + return { + items: Ox._('All {0}', [Ox._(pandora.site.itemName.plural)]), + documents: Ox._('All {0}', [Ox._('Documents')]) + }[section] || Ox._('{0} ' + Ox.toTitleCase(section), [pandora.site.site.name]); }; pandora.getClipData = function(items) { @@ -1068,13 +1236,19 @@ pandora.getClipVideos = function(clip, resolution) { }; (function() { - var itemTitles = {}; - pandora.getDocumentTitle = function(itemData) { + var itemTitles = {}, documentTitles = {}; + pandora.getWindowTitle = function(itemData) { var parts = []; if (itemData) { - itemTitles[pandora.user.ui.item] = Ox.decodeHTMLEntities( - pandora.getItemTitle(itemData) - ); + if (pandora.user.ui.section == 'documents') { + documentTitles[pandora.user.ui.document] = Ox.decodeHTMLEntities( + pandora.getDocumentTitle(itemData) + ); + } else { + itemTitles[pandora.user.ui.item] = Ox.decodeHTMLEntities( + pandora.getItemTitle(itemData) + ); + } } if (pandora.user.ui.section == 'items') { if (!pandora.user.ui.item) { @@ -1094,16 +1268,33 @@ pandora.getClipVideos = function(clip, resolution) { Ox._(Ox.toTitleCase(pandora.user.ui.itemView)) ])); } + } else if (pandora.user.ui.section == 'documents') { + if (!pandora.user.ui.document) { + parts.push( + pandora.user.ui._collection + ? pandora.user.ui._collection.split(':').slice(1).join(':') + : pandora.getAllItemsTitle('documents') + ); + parts.push(Ox._('{0} View', [ + Ox._(Ox.toTitleCase(pandora.user.ui.collectionView)) + ])); + parts.push(Ox._('Documents')); + } else { + parts.push( + documentTitles[pandora.user.ui.document] || pandora.user.ui.document + ); + /* + parts.push(Ox._('{0} View', [ + Ox._(Ox.toTitleCase(pandora.user.ui.documentView)) + ])); + */ + parts.push(Ox._('Document')); + } } else if (pandora.user.ui.section == 'edits') { if (pandora.user.ui.edit) { parts.push(pandora.user.ui.edit.split(':').slice(1).join(':')); } parts.push(Ox._('Edits')); - } else if (pandora.user.ui.section == 'texts') { - if (pandora.user.ui.text) { - parts.push(pandora.user.ui.text.split(':').slice(1).join(':')); - } - parts.push(Ox._('Texts')); } parts.push(pandora.site.site.name); return parts.join(' – '); @@ -1138,6 +1329,12 @@ pandora.getFilterSizes = function() { ); }; +pandora.getFolderItems = function(section) { + return section == 'items' ? 'Lists' + : section == 'documents' ? 'Collections' + : Ox.toTitleCase(section); +} + pandora.getFoldersHeight = function(section) { section = section || pandora.user.ui.section; var height = 0; @@ -1299,6 +1496,16 @@ pandora.getItem = function(state, str, callback) { }); } }); + } else if (state.type == 'documents') { + pandora.api.getDocument({id: str, keys: ['id']}, function(result) { + if (result.status.code == 200) { + state.item = result.data.id; + callback(); + } else { + state.item = ''; + callback(); + } + }); } else if (state.type == 'edits') { pandora.api.getEdit({id: str, keys: ['id']}, function(result) { if (result.status.code == 200) { @@ -1310,21 +1517,23 @@ pandora.getItem = function(state, str, callback) { } }); } else if (state.type == 'texts') { - pandora.api.getText({ - id: str, - keys: ['id', 'names', 'pages', 'type'] + pandora.api.findDocuments({ + query: { + conditions: [ + {key: 'user', value: str.split(':')[0]}, + {key: 'title', value: str.split(':').slice(1).join(':')} + ], + operator: '&'}, + keys: ['id', 'extension'], + range: [0, 2] }, function(result) { - if (result.status.code == 200) { - state.item = result.data.id; - callback(); + state.type = 'documents'; + if (result.data.items.length == 1) { + state.item = result.data.items[0].id; } else { - // FIXME: add findText call here? - // FIXME: it's obscure that in the texts case, - // we have to set item to '', while for videos, - // it remains undefined state.item = ''; - callback(); } + callback(); }); } else { callback(); @@ -1471,12 +1680,19 @@ pandora.getLargeEditTimelineURL = function(edit, type, i, callback) { }; pandora.getListData = function(list) { - var data = {}, folder; + var data = {}, folder, _list = pandora.user.ui._list; if (Ox.isUndefined(list)) { - list = pandora.user.ui[ - pandora.user.ui.section == 'items' ? '_list' - : pandora.user.ui.section.slice(0, -1) - ]; + if (pandora.user.ui.section == 'items') { + list = pandora.user.ui._list; + } else if (pandora.user.ui.section == 'documents') { + list = pandora.user.ui._collection; + _list = pandora.user.ui._collection; + } else { + list = pandora.user.ui.section.slice(0, -1) + } + } + if (pandora.user.ui.section == 'documents') { + _list = pandora.user.ui._collection; } if (list && pandora.$ui.folderList) { Ox.forEach(pandora.$ui.folderList, function($list, id) { @@ -1485,7 +1701,7 @@ pandora.getListData = function(list) { // folder it is selected, since for example, a personal // list may appear again in the featured lists browser if ( - (list == pandora.user.ui._list && $list.options('selected').length) + (list == _list && $list.options('selected').length) || !Ox.isEmpty($list.value(list)) ) { folder = id; @@ -1506,6 +1722,16 @@ pandora.getListData = function(list) { return data; }; +pandora.getListIcon = function(section, id, size, modified) { + var folderItems = pandora.getFolderItems(section), + folderItem = folderItems.slice(0, -1); + size = size || ''; + modified = modified || Ox.uid(); + return pandora.getMediaURL('/' + + folderItem.toLowerCase() + '/' + + encodeURIComponent(id) + '/icon' + size + '.jpg?' + modified); +}; + pandora.getPageTitle = function(stateOrURL) { var pages = [ {id: '', title: ''}, @@ -1529,6 +1755,7 @@ pandora.getPageTitle = function(stateOrURL) { }; pandora.getPart = function(state, str, callback) { + Ox.Log('URL', 'getPart', state, str); if (state.page == 'api') { pandora.api.api(function(result) { if (Ox.contains(Object.keys(result.data.actions), str)) { @@ -1656,6 +1883,8 @@ pandora.getSort = function(state, val, callback) { // TODO in the future: If str is index, fall back if list is smart // (but this can only be tested after find has been parsed) callback(); + } else if (state.type == 'documents') { + callback(); } else if (state.type == 'edits') { if (val[0].key == 'index') { pandora.api.getEdit({id: state.item, keys: ['id', 'type']}, function(result) { @@ -1675,8 +1904,6 @@ pandora.getSort = function(state, val, callback) { } else { callback(); } - } else if (state.type == 'texts') { - callback(); } }; @@ -1705,6 +1932,29 @@ pandora.getSortOperator = function(key) { ) > -1 ? '+' : '-'; }; +pandora.getDocumentSortKeys = function() { + return pandora.site.documentKeys.filter(function(key) { + return key.sort && ( + !key.capability + || pandora.site.capabilities[key.capability][pandora.user.level] + ); + }).map(function(key) { + return Ox.extend(key, { + operator: pandora.getDocumentSortOperator(key.id) + }); + }); +}; +pandora.getDocumentSortOperator = function(key) { + var data = Ox.getObjectById(pandora.site.documentKeys, key); + return data.sortOperator || ['string', 'text'].indexOf( + Ox.isArray(data.type) ? data.type[0] : data.type + ) > -1 ? '+' : '-'; +}; + +pandora.getDocumentTitle = function(data) { + return data.title || Ox._('Untitled'); +}; + pandora.getSpan = function(state, val, callback) { // For a given item, or none (state.item), and a given view, or any // (state.view), this takes a value (array of numbers or string) and checks @@ -1712,9 +1962,10 @@ pandora.getSpan = function(state, val, callback) { // event/place name (string), and in that case sets state.span, and may // modify state.view. // fixme: "subtitles:23" is still missing - if (state.page == 'documents') { + Ox.Log('URL', 'getSpan', state, val); + if (state.type == 'documents') { pandora.api.getDocument({ - id: state.part, + id: state.item, keys: ['dimensions', 'extension'] }, function(result) { var dimensions = result.data.dimensions, @@ -1722,6 +1973,8 @@ pandora.getSpan = function(state, val, callback) { values; if (Ox.contains(['epub', 'pdf', 'txt'], extension)) { state.span = Ox.limit(parseInt(val), 1, dimensions); + } else if (Ox.contains(['html'], extension)) { + state.span = Ox.limit(parseInt(val), 0, 100); } else if (Ox.contains(['gif', 'jpg', 'png'], extension)) { values = val.split(','); if (values.length == 4) { @@ -1738,6 +1991,7 @@ pandora.getSpan = function(state, val, callback) { state.span = ''; } } + Ox.Log('URL', 'getSpan result', state); callback(); }); } else if (state.type == pandora.site.itemName.plural.toLowerCase()) { @@ -1817,24 +2071,6 @@ pandora.getSpan = function(state, val, callback) { callback(); }); } - } else if (state.type == 'texts') { - pandora.api.getText({id: state.item}, function(result) { - if (isArray) { - if (result.data.type == 'html') { - state.span = Ox.limit(val[0], 0, 100); - } else { - state.span = Math.floor( - Ox.limit(val[0], 1, result.data.pages) - ); - } - } else if ( - result.data.type == 'html' - && Ox.contains(result.data.names, val) - ) { - state.span = val; - } - callback(); - }); } function getId(type, callback) { @@ -1898,6 +2134,9 @@ pandora.getStatusText = function(data) { data.items == 1 ? 'singular' : 'plural' ]), parts = []; + if (ui.section == 'documents') { + itemName = Ox._(Ox.toTitleCase(data.items == 1 ? ui.section.slice(0, -1) : ui.section)); + } parts.push(Ox.formatNumber(data.items) + ' '+ itemName); if (data.runtime) { parts.push(Ox.formatDuration(data.runtime, 'short')); @@ -2263,6 +2502,8 @@ pandora.signin = function(data) { pandora.user.ui._list = pandora.getListState(pandora.user.ui.find); pandora.user.ui._filterState = pandora.getFilterState(pandora.user.ui.find); pandora.user.ui._findState = pandora.getFindState(pandora.user.ui.find); + pandora.user.ui._collection = pandora.getCollectionState(pandora.user.ui.findDocuments); + pandora.user.ui._findDocumentsState = pandora.getFindDocumentsState(pandora.user.ui.findDocuments); pandora.site.sortKeys = pandora.getSortKeys(); pandora.URL.init(); pandora.URL.update(); @@ -2277,6 +2518,8 @@ pandora.signout = function(data) { pandora.user.ui._list = pandora.getListState(pandora.user.ui.find); pandora.user.ui._filterState = pandora.getFilterState(pandora.user.ui.find); pandora.user.ui._findState = pandora.getFindState(pandora.user.ui.find); + pandora.user.ui._collection = pandora.getCollectionState(pandora.user.ui.findDocuments); + pandora.user.ui._findDocumentsState = pandora.getFindDocumentsState(pandora.user.ui.findDocuments); pandora.site.sortKeys = pandora.getSortKeys(); pandora.URL.init(); pandora.URL.update(); @@ -2288,9 +2531,11 @@ pandora.reloadList = function() { Ox.Log('', 'reloadList') var listData = pandora.getListData(); Ox.Request.clearCache(); // fixme: remove - pandora.$ui.filters.forEach(function($filter) { - $filter.reloadList(); - }); + if (pandora.$ui.filters) { + pandora.$ui.filters.forEach(function($filter) { + $filter.reloadList(); + }); + } pandora.$ui.list .bindEvent({ init: function(data) { @@ -2336,6 +2581,20 @@ pandora.renameList = function(oldId, newId, newName, folder) { } }, false); pandora.UI.set('lists.' + pandora.UI.encode(oldId), null, false); + } else if (pandora.user.ui.section == 'documents') { + pandora.replaceURL = true; + pandora.UI.set( + 'collections.' + pandora.UI.encode(newId), + pandora.user.ui.lists[oldId], + false + ); + pandora.UI.set({ + findDocuments: { + conditions: [{key: 'collection', value: newId, operator: '=='}], + operator: '&' + } + }, false); + pandora.UI.set('collections.' + pandora.UI.encode(oldId), null, false); } else { pandora.replaceURL = true; pandora.UI.set( @@ -2380,7 +2639,7 @@ pandora.resizeFolders = function(section) { userColumnWidth = Math.round(columnWidth * 0.4), nameColumnWidth = columnWidth - userColumnWidth; pandora.$ui.allItems && pandora.$ui.allItems.resizeElement(( - section == 'items' ? columnWidth + Ox.contains(pandora.site.listSections, section) ? columnWidth : section == 'edits' ? width - 16 : width - 48 ) - 8); @@ -2489,6 +2748,12 @@ pandora.resizeWindow = function() { pandora.$ui.calendar.resizeCalendar(); } } + } else if (pandora.user.ui.section == 'documents') { + if (pandora.user.ui.document) { + pandora.$ui.document && pandora.$ui.document.update(); + } else { + pandora.$ui.list && pandora.$ui.list.size(); + } } else if (pandora.user.ui.section == 'edits') { if (!pandora.user.ui.edit) { // ... @@ -2537,6 +2802,38 @@ pandora.selectList = function() { } }); } + } else if (pandora.user.ui.section == 'documents') { + if (pandora.user.ui._collection) { + pandora.api.findCollections({ + keys: ['status', 'user'], + query: { + conditions: [{ + key: 'id', + operator: '==', + value: pandora.user.ui._collection + }], + operator: '' + }, + range: [0, 1] + }, function(result) { + var folder, list; + if (result.data.items.length) { + list = result.data.items[0]; + folder = list.status == 'featured' ? 'featured' : ( + list.user == pandora.user.username + ? 'personal' : 'favorite' + ); + pandora.$ui.folderList[folder] + .options({selected: [pandora.user.ui._collection]}); + if ( + !pandora.hasDialogOrScreen() + && !Ox.Focus.focusedElementIsInput() + ) { + pandora.$ui.folderList[folder].gainFocus(); + } + } + }); + } } else { var id = pandora.user.ui[pandora.user.ui.section.slice(0,-1)], section = Ox.toTitleCase(pandora.user.ui.section.slice(0, -1)); @@ -2891,18 +3188,76 @@ pandora.wait = function(id, callback, timeout) { } return state; }; - - pandora.getListState = function(find) { - // A list is selected if exactly one condition in an & query has "list" - // as key and "==" as operator + function getState(find, key) { var index, state = ''; if (find.operator == '&') { - index = oneCondition(find.conditions, 'list', '=='); + index = oneCondition(find.conditions, key, '=='); if (index > -1) { state = find.conditions[index].value; } } return state; + } + + pandora.getCollectionState = function(find) { + // A collection is selected if exactly one condition in an & query has "collection" + // as key and "==" as operator + return getState(find, 'collection'); + }; + + pandora.getListState = function(find) { + // A list is selected if exactly one condition in an & query has "list" + // as key and "==" as operator + return getState(find, 'list'); + }; + + pandora.getFindDocumentsState = function(find) { + // The find element is populated if exactly one condition in an & query + // has a findKey as key and "=" as operator (and all other conditions + // are either list or filters), or if all conditions in an | query have + // the same filter id as key and "==" as operator + Ox.Log('Find', 'getFindDocumentsState', find) + var conditions, indices, state = {index: -1, key: '*', value: ''}; + if (find.operator == '&') { + // number of conditions that are not list or filters + conditions = find.conditions.length + - !!pandora.user.ui._collection; + /* + - pandora.user.ui._filterState.filter(function(filter) { + return filter.index > -1; + }).length; + */ + // indices of non-advanced find queries + indices = pandora.site.documentKeys.map(function(findKey) { + return oneCondition(find.conditions, findKey.id, '='); + }).filter(function(index) { + return index > -1; + }); + state = conditions == 1 && indices.length == 1 ? { + index: indices[0], + key: find.conditions[indices[0]].key, + value: Ox.decodeURIComponent(find.conditions[indices[0]].value) + } : { + index: -1, + key: conditions == 0 && indices.length == 0 ? '*' : 'advanced', + value: '' + }; + } else { + state = { + index: -1, + key: 'advanced', + value: '' + }; + /* + Ox.forEach(pandora.user.ui.documentFilters, function(key) { + if (everyCondition(find.conditions, key, '==')) { + state.key = '*'; + return false; + } + }); + */ + } + return state; }; }()); diff --git a/static/png/cover.png b/static/png/cover.png new file mode 100644 index 00000000..6d919562 Binary files /dev/null and b/static/png/cover.png differ diff --git a/update.py b/update.py index fad1c643..5a2abc86 100755 --- a/update.py +++ b/update.py @@ -216,6 +216,8 @@ if __name__ == "__main__": run('./bin/pip', 'install', '-r', 'requirements.txt') update_service('pandora-encoding') update_service('pandora-tasks') + if old <= 5673: + run('./pandora/manage.py', 'rebuild_documentfind') else: if len(sys.argv) == 1: release = get_release()