diff --git a/webnotes/auth.py b/webnotes/auth.py index e0bc737c6c..fba197667b 100644 --- a/webnotes/auth.py +++ b/webnotes/auth.py @@ -107,7 +107,7 @@ class LoginManager: def set_user_info(self): info = webnotes.conn.get_value("Profile", self.user, - ["user_type", "first_name", "last_name"], as_dict=1) + ["user_type", "first_name", "last_name", "user_image"], as_dict=1) if info.user_type=="Website User": webnotes._response.set_cookie("system_user", "no") webnotes.response["message"] = "No App" @@ -119,6 +119,7 @@ class LoginManager: webnotes.response["full_name"] = full_name webnotes._response.set_cookie("full_name", full_name) webnotes._response.set_cookie("user_id", self.user) + webnotes._response.set_cookie("user_image", info.user_image or "") def make_session(self, resume=False): # start session diff --git a/webnotes/core/doctype/profile/profile.py b/webnotes/core/doctype/profile/profile.py index 66fb92ebe8..e39cf97830 100644 --- a/webnotes/core/doctype/profile/profile.py +++ b/webnotes/core/doctype/profile/profile.py @@ -347,6 +347,57 @@ def reset_password(user): else: return "No such user (%s)" % user +@webnotes.whitelist(allow_guest=True) +def facebook_login(data): + data = json.loads(data) + + if not (data.get("id") and data.get("fb_access_token")): + raise webnotes.ValidationError + + user = data["email"] + + if not get_fb_userid(data.get("fb_access_token")): + # garbage + raise webnotes.ValidationError + + if not webnotes.conn.exists("Profile", user): + if data.get("birthday"): + b = data.get("birthday").split("/") + data["birthday"] = b[2] + "-" + b[0] + "-" + b[1] + + profile = webnotes.bean({ + "doctype":"Profile", + "first_name": data["first_name"], + "last_name": data["last_name"], + "email": data["email"], + "enabled": 1, + "new_password": webnotes.generate_hash(data["email"]), + "fb_username": data["username"], + "fb_userid": data["id"], + "fb_location": data.get("location", {}).get("name"), + "fb_hometown": data.get("hometown", {}).get("name"), + "fb_age_range": data.get("age_range") and "{min}-{max}".format(**data.get("age_range")), + "birth_date": data.get("birthday"), + "fb_bio": data.get("bio"), + "fb_education": data.get("education") and data.get("education")[-1].get("type"), + "user_type": "Website User" + }) + profile.ignore_permissions = True + profile.get_controller().no_welcome_mail = True + profile.insert() + + webnotes.local.login_manager.user = user + webnotes.local.login_manager.post_login() + +def get_fb_userid(fb_access_token): + import requests + response = requests.get("https://graph.facebook.com/me?access_token=" + fb_access_token) + if response.status_code==200: + print response.json() + return response.json().get("id") + else: + return webnotes.AuthenticationError + def profile_query(doctype, txt, searchfield, start, page_len, filters): from webnotes.widgets.reportview import get_match_cond return webnotes.conn.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) diff --git a/webnotes/public/build.json b/webnotes/public/build.json index ce627b9139..68a807e494 100644 --- a/webnotes/public/build.json +++ b/webnotes/public/build.json @@ -7,10 +7,17 @@ ], "js/webnotes-web.min.js": [ "public/js/lib/bootstrap.min.js", + "public/js/wn/provide.js", "public/js/wn/misc/number_format.js", "public/js/lib/nprogress.js", "public/js/wn/translate.js", - "website/js/website.js" + "public/js/wn/misc/pretty_date.js", + "website/js/website.js", + "website/js/website_group.js" + ], + "js/canvasResize.min.js": [ + "website/js/jquery.exif.js", + "website/js/jquery.canvasResize.js" ], "js/editor.min.js": [ "public/js/lib/jquery/jquery.hotkeys.js", diff --git a/webnotes/public/css/common.css b/webnotes/public/css/common.css index 422dd78245..d0bc1c77d3 100644 --- a/webnotes/public/css/common.css +++ b/webnotes/public/css/common.css @@ -316,6 +316,11 @@ div#freeze { max-height: 15px; } +.navbar-brand { + min-height: 20px; + height: auto; +} + .navbar #spinner { display: block; float: right; diff --git a/webnotes/templates/generators/website_group.html b/webnotes/templates/generators/website_group.html index ee5a840c07..d5213f4881 100644 --- a/webnotes/templates/generators/website_group.html +++ b/webnotes/templates/generators/website_group.html @@ -11,25 +11,23 @@ -{% include view.template_path %} - -{%- if access -%} -{%- endif -%} + +{% include view.template_path %} + {% endblock %} {% block sidebar %}{% include "templates/includes/sidebar.html" %}{% endblock %} \ No newline at end of file diff --git a/webnotes/templates/generators/website_group.py b/webnotes/templates/generators/website_group.py index 70683da517..0badc16a9b 100644 --- a/webnotes/templates/generators/website_group.py +++ b/webnotes/templates/generators/website_group.py @@ -35,7 +35,7 @@ def get_context(context): def get_group_context(group, view, bean): cache_key = "website_group_context:{}:{}".format(group, view) views = get_views(bean.doc.group_type) - view = views.get(view) + view = webnotes._dict(views.get(view)) if can_cache(view.get("no_cache")): group_context = webnotes.cache().get_value(cache_key) @@ -86,9 +86,10 @@ def get_handler(group_type): return webnotes.get_module(handler[0]) def get_views(group_type): + from copy import deepcopy handler = get_handler(group_type) if handler and hasattr(handler, "get_views"): - return handler.get_views() + return deepcopy(handler.get_views() or {}) return {} def has_access(group, view): diff --git a/webnotes/templates/includes/inline_post.html b/webnotes/templates/includes/inline_post.html index e3a4fd4183..8d02b964da 100644 --- a/webnotes/templates/includes/inline_post.html +++ b/webnotes/templates/includes/inline_post.html @@ -20,10 +20,10 @@ {%- endif -%} diff --git a/webnotes/templates/includes/post_editor.html b/webnotes/templates/includes/post_editor.html new file mode 100644 index 0000000000..090d5506dc --- /dev/null +++ b/webnotes/templates/includes/post_editor.html @@ -0,0 +1,66 @@ +{% set parent_post = post.parent_post if post else parent_post %} +
+ {%- if not (post and post.parent_post) and not parent_post-%} + + {%- endif -%} + + + + {%- if view.name != "post" and not (post and post.parent_post) -%} + {%- if group.group_type == "Tasks" -%} + + +
+
+
+ +
+
+ × +
+
+
+
+ + +
+ {%- elif group.group_type == "Events" -%} + + + {%- endif -%} + {%- endif -%} + +
tab + enter to post / markdown formatting
+
+ +
+
+ + + + + +
+
\ No newline at end of file diff --git a/webnotes/templates/includes/profile_display.html b/webnotes/templates/includes/profile_display.html new file mode 100644 index 0000000000..23846020d4 --- /dev/null +++ b/webnotes/templates/includes/profile_display.html @@ -0,0 +1,9 @@ +
+
+ +
+
+
{{ profile.first_name or "" }} {{ profile.last_name or "" }}
+
{{ profile.fb_location or profile.fb_hometown or "" }}
+
+
\ No newline at end of file diff --git a/webnotes/templates/includes/sidebar.html b/webnotes/templates/includes/sidebar.html index 7983a5e87d..d2fa926986 100644 --- a/webnotes/templates/includes/sidebar.html +++ b/webnotes/templates/includes/sidebar.html @@ -1,7 +1,7 @@ {%- if children -%} {%- for child in children -%}
+ {%- if fb_app_id is defined -%} +
+

+ {%- endif -%}

diff --git a/webnotes/templates/pages/message.py b/webnotes/templates/pages/message.py index f7822b8dc0..2b512e05a4 100644 --- a/webnotes/templates/pages/message.py +++ b/webnotes/templates/pages/message.py @@ -13,7 +13,8 @@ def get_context(context): if hasattr(webnotes.local, "message"): message_context["title"] = webnotes.local.message_title message_context["message"] = webnotes.local.message - message_context["success"] = webnotes.local.message_success + if hasattr(webnotes.local, "message_success"): + message_context["success"] = webnotes.local.message_success message_context.update(context) return render_blocks(message_context) diff --git a/webnotes/templates/website_group/edit_post.html b/webnotes/templates/website_group/edit_post.html index e69de29bb2..684b69a101 100644 --- a/webnotes/templates/website_group/edit_post.html +++ b/webnotes/templates/website_group/edit_post.html @@ -0,0 +1,19 @@ +{% include "templates/includes/post_editor.html" %} + + + diff --git a/webnotes/templates/website_group/events.html b/webnotes/templates/website_group/events.html index e69de29bb2..f379de8494 100644 --- a/webnotes/templates/website_group/events.html +++ b/webnotes/templates/website_group/events.html @@ -0,0 +1,29 @@ +
+
+{{ post_list_html }} +
+
+ +
+ + diff --git a/webnotes/templates/website_group/events.py b/webnotes/templates/website_group/events.py new file mode 100644 index 0000000000..d4a639c94e --- /dev/null +++ b/webnotes/templates/website_group/events.py @@ -0,0 +1,125 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import webnotes +from webnotes.utils import now_datetime, get_datetime_str +from webnotes.webutils import get_access +from webnotes.templates.website_group.settings import get_settings_context +from webnotes.templates.website_group.post import get_post_context + +def get_views(): + return views + +def get_context(group_context): + events_context = {} + + if group_context.view.name in ("upcoming", "past"): + events_context["post_list_html"] = get_post_list_html(group_context["group"]["name"], group_context["view"]) + + elif group_context.view.name == "edit": + events_context["session_user"] = webnotes.session.user + events_context["post"] = webnotes.doc("Post", webnotes.form_dict.name).fields + + elif group_context.view.name == "settings": + events_context.update(get_settings_context(group_context)) + + elif group_context.view.name == "post": + events_context.update(get_post_context(group_context)) + + return events_context + +@webnotes.whitelist(allow_guest=True) +def get_post_list_html(group, view, limit_start=0, limit_length=20): + access = get_access(group) + + if isinstance(view, basestring): + view = get_views()[view] + + view = webnotes._dict(view) + + # verify permission for paging + if webnotes.local.form_dict.cmd == "get_post_list_html": + if not access.get("read"): + return webnotes.PermissionError + + if view.name=="upcoming": + condition = "and p.event_datetime >= %s" + order_by = "p.event_datetime asc" + else: + condition = "and p.event_datetime < %s" + order_by = "p.event_datetime desc" + + # should show based on time upto precision of hour + # because the current hour should also be in upcoming + now = now_datetime().replace(minute=0, second=0, microsecond=0) + + posts = webnotes.conn.sql("""select p.*, pr.user_image, pr.first_name, pr.last_name, + (select count(pc.name) from `tabPost` pc where pc.parent_post=p.name) as post_reply_count + from `tabPost` p, `tabProfile` pr + where p.website_group = %s and pr.name = p.owner and ifnull(p.parent_post, '')='' + and p.is_event=1 {condition} + order by {order_by} limit %s, %s""".format(condition=condition, order_by=order_by), + (group, now, int(limit_start), int(limit_length)), as_dict=True) + + context = {"posts": posts, "limit_start": limit_start, "view": view} + + return webnotes.get_template("templates/includes/post_list.html").render(context) + +views = { + "upcoming": { + "name": "upcoming", + "template_path": "templates/website_group/events.html", + "url": "/{group}", + "label": "Upcoming", + "icon": "icon-calendar", + "default": True, + "idx": 1 + }, + "past": { + "name": "past", + "template_path": "templates/website_group/events.html", + "url": "/{group}?view=past", + "label": "Past", + "icon": "icon-time", + "idx": 2 + }, + "post": { + "name": "post", + "template_path": "templates/website_group/post.html", + "url": "/{group}?view=post&name={post}", + "label": "Post", + "icon": "icon-comments", + "hidden": True, + "no_cache": True, + "idx": 3 + }, + "edit": { + "name": "edit", + "template_path": "templates/website_group/edit_post.html", + "url": "/{group}?view=edit&name={post}", + "label": "Edit Post", + "icon": "icon-pencil", + "hidden": True, + "no_cache": True, + "idx": 4 + }, + "add": { + "name": "add", + "template_path": "templates/website_group/edit_post.html", + "url": "/{group}?view=add", + "label": "Add Post", + "icon": "icon-plus", + "hidden": True, + "idx": 5 + }, + "settings": { + "name": "settings", + "template_path": "templates/website_group/settings.html", + "url": "/{group}?view=settings", + "label": "Settings", + "icon": "icon-cog", + "hidden": True, + "idx": 6 + } +} \ No newline at end of file diff --git a/webnotes/templates/website_group/forum.html b/webnotes/templates/website_group/forum.html index 6158ac3e15..fc7989c014 100644 --- a/webnotes/templates/website_group/forum.html +++ b/webnotes/templates/website_group/forum.html @@ -1,4 +1,3 @@ -{%- block view -%}
{{ post_list_html }} @@ -6,17 +5,24 @@
-{%- endblock -%} -{%- block group_script -%} -{%- endblock -%} \ No newline at end of file diff --git a/webnotes/templates/website_group/forum.py b/webnotes/templates/website_group/forum.py index df485a42fc..f711ab7f11 100644 --- a/webnotes/templates/website_group/forum.py +++ b/webnotes/templates/website_group/forum.py @@ -5,22 +5,36 @@ from __future__ import unicode_literals import webnotes from webnotes.utils import now_datetime, get_datetime_str from webnotes.webutils import get_access -from webnotes.templates.generators.website_group import get_views +from webnotes.templates.website_group.settings import get_settings_context +from webnotes.templates.website_group.post import get_post_context def get_views(): return views def get_context(group_context): - return { - "post_list_html": get_post_list_html(group_context["group"]["name"], group_context["view"]) - } + forum_context = {} + + if group_context.view.name in ("popular", "feed"): + forum_context["post_list_html"] = get_post_list_html(group_context["group"]["name"], group_context["view"]) + + elif group_context.view.name == "edit": + forum_context["session_user"] = webnotes.session.user + forum_context["post"] = webnotes.doc("Post", webnotes.form_dict.name).fields + + elif group_context.view.name == "settings": + forum_context.update(get_settings_context(group_context)) + + elif group_context.view.name == "post": + forum_context.update(get_post_context(group_context)) + + return forum_context @webnotes.whitelist(allow_guest=True) def get_post_list_html(group, view, limit_start=0, limit_length=20): access = get_access(group) if isinstance(view, basestring): - view = get_views(group)["view"] + view = get_views()[view] view = webnotes._dict(view) @@ -50,7 +64,7 @@ def get_post_list_html(group, view, limit_start=0, limit_length=20): views = { "popular": { "name": "popular", - "template_path": "templates/unit_templates/forum_list.html", + "template_path": "templates/website_group/forum.html", "url": "/{group}", "label": "Popular", "icon": "icon-heart", @@ -60,7 +74,7 @@ views = { }, "feed": { "name": "feed", - "template_path": "templates/unit_templates/forum_list.html", + "template_path": "templates/website_group/forum.html", "url": "/{group}?view=feed", "label": "Feed", "icon": "icon-rss", @@ -69,7 +83,7 @@ views = { }, "post": { "name": "post", - "template_path": "templates/unit_templates/base_post.html", + "template_path": "templates/website_group/post.html", "url": "/{group}?view=post&name={post}", "label": "Post", "icon": "icon-comments", @@ -80,7 +94,7 @@ views = { }, "edit": { "name": "edit", - "template_path": "templates/unit_templates/base_edit.html", + "template_path": "templates/website_group/edit_post.html", "url": "/{group}?view=edit&name={post}", "label": "Edit Post", "icon": "icon-pencil", @@ -90,7 +104,7 @@ views = { }, "add": { "name": "add", - "template_path": "templates/unit_templates/base_edit.html", + "template_path": "templates/website_group/edit_post.html", "url": "/{group}?view=add", "label": "Add Post", "icon": "icon-plus", @@ -99,8 +113,8 @@ views = { }, "settings": { "name": "settings", - "template_path": "templates/unit_templates/base_settings.html", - "url": "/{group}&view=settings", + "template_path": "templates/website_group/settings.html", + "url": "/{group}?view=settings", "label": "Settings", "icon": "icon-cog", "hidden": True, diff --git a/webnotes/templates/website_group/post.html b/webnotes/templates/website_group/post.html index e69de29bb2..1290a659d3 100644 --- a/webnotes/templates/website_group/post.html +++ b/webnotes/templates/website_group/post.html @@ -0,0 +1,34 @@ +
+
{{ parent_post_html }}
+
+ +
+
+{{ post_list_html }} +
+
+{% include "templates/includes/post_editor.html" %} +
+ + + diff --git a/webnotes/templates/website_group/post.py b/webnotes/templates/website_group/post.py new file mode 100644 index 0000000000..32a42796fa --- /dev/null +++ b/webnotes/templates/website_group/post.py @@ -0,0 +1,41 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import webnotes +from webnotes.utils import get_fullname + +def get_post_context(group_context): + post = webnotes.doc("Post", webnotes.form_dict.name) + if post.parent_post: + raise webnotes.PermissionError + + fullname = get_fullname(post.owner) + return { + "title": "{} by {}".format(post.title, fullname), + # "group_title": group_context.get("unit_title") + " by {}".format(fullname), + "parent_post_html": get_parent_post_html(post, group_context.get("view")), + "post_list_html": get_child_posts_html(post, group_context.get("view")), + "parent_post": post.name + } + +def get_parent_post_html(post, view): + profile = webnotes.bean("Profile", post.owner).doc + for fieldname in ("first_name", "last_name", "user_image", "fb_hometown", "fb_location"): + post.fields[fieldname] = profile.fields[fieldname] + + return webnotes.get_template("templates/includes/inline_post.html")\ + .render({"post": post.fields, "view": view}) + +def get_child_posts_html(post, view): + posts = webnotes.conn.sql("""select p.*, pr.user_image, pr.first_name, pr.last_name + from tabPost p, tabProfile pr + where p.parent_post=%s and pr.name = p.owner + order by p.creation asc""", (post.name,), as_dict=True) + + return webnotes.get_template("templates/includes/post_list.html")\ + .render({ + "posts": posts, + "parent_post": post.name, + "view": view + }) \ No newline at end of file diff --git a/webnotes/templates/website_group/settings.html b/webnotes/templates/website_group/settings.html index e69de29bb2..32d230875b 100644 --- a/webnotes/templates/website_group/settings.html +++ b/webnotes/templates/website_group/settings.html @@ -0,0 +1,77 @@ +
+
+

1. Edit Description

+
+
+

+ +

+
+ +
+
+
+
+

2. Add Sub Groups

+
+
+
+ +

Only letters, numbers and spaces

+
+
+ + +
+
+ +

Private if unchecked, only users with explicit read access will be allowed to read

+
+
+ +
+
+ +
+
+
+
+

3. Manage Users

+ +
+ + + + + + + + + + + {% for profile in profiles %} + {% include "templates/includes/sitemap_permission.html" %} + {% endfor %} + +
UserReadWriteAdmin
+
+
+ + diff --git a/webnotes/templates/website_group/settings.py b/webnotes/templates/website_group/settings.py new file mode 100644 index 0000000000..56624070c1 --- /dev/null +++ b/webnotes/templates/website_group/settings.py @@ -0,0 +1,105 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import webnotes +from webnotes.webutils import get_access +from webnotes.website.doctype.website_sitemap_permission.website_sitemap_permission import clear_permissions +from webnotes.utils.email_lib.bulk import send + +def get_settings_context(group_context): + if not get_access(group_context.group.name).get("admin"): + raise webnotes.PermissionError + + return { + "profiles": webnotes.conn.sql("""select p.*, wsp.`read`, wsp.`write`, wsp.`admin` + from `tabProfile` p, `tabWebsite Sitemap Permission` wsp + where wsp.website_sitemap=%s and wsp.profile=p.name""", (group_context.group.name,), as_dict=True) + } + +@webnotes.whitelist() +def suggest_user(term, group): + profiles = webnotes.conn.sql("""select pr.name, pr.first_name, pr.last_name, + pr.user_image, pr.fb_location, pr.fb_hometown + from `tabProfile` pr + where (pr.first_name like %(term)s or pr.last_name like %(term)s) + and pr.user_image is not null and pr.enabled=1 + and not exists(select wsp.name from `tabWebsite Sitemap Permission` wsp + where wsp.website_sitemap=%(group)s and wsp.profile=pr.name)""", + {"term": "%{}%".format(term), "group": group}, as_dict=True) + + template = webnotes.get_template("templates/includes/profile_display.html") + return [{ + "value": "{} {}".format(pr.first_name, pr.last_name), + "profile_html": template.render({"profile": pr}), + "profile": pr.name + } for pr in profiles] + +@webnotes.whitelist() +def add_sitemap_permission(sitemap_page, profile): + if not get_access(sitemap_page).get("admin"): + raise webnotes.PermissionError + + permission = webnotes.bean({ + "doctype": "Website Sitemap Permission", + "website_sitemap": sitemap_page, + "profile": profile, + "read": 1 + }) + permission.insert(ignore_permissions=True) + + profile = permission.doc.fields + profile.update(webnotes.conn.get_value("Profile", profile.profile, + ["name", "first_name", "last_name", "user_image", "fb_location", "fb_hometown"], as_dict=True)) + + return webnotes.get_template("templates/includes/sitemap_permission.html").render({ + "profile": profile + }) + +@webnotes.whitelist() +def update_permission(sitemap_page, profile, perm, value): + if not get_access(sitemap_page).get("admin"): + raise webnotes.PermissionError + + permission = webnotes.bean("Website Sitemap Permission", {"website_sitemap": sitemap_page, "profile": profile}) + permission.doc.fields[perm] = int(value) + permission.save(ignore_permissions=True) + + # send email + if perm=="admin" and int(value): + group_title = webnotes.conn.get_value("Website Sitemap", sitemap_page, "page_title") + + subject = "You have been made Administrator of Group " + group_title + + send(recipients=[profile], + subject= subject, add_unsubscribe_link=False, + message="""

Group Notification

\ +

%s

\ +

This is just for your information.

""" % subject) + +@webnotes.whitelist() +def update_description(group, description): + if not get_access(group).get("admin"): + raise webnotes.PermissionError + + group = webnotes.bean("Website Group", group) + group.doc.group_description = description + group.save(ignore_permissions=True) + +@webnotes.whitelist() +def add_website_group(group, new_group, public_read, public_write, group_type="Forum"): + if not get_access(group).get("admin"): + raise webnotes.PermissionError + + parent_website_sitemap = webnotes.conn.get_value("Website Sitemap", + {"ref_doctype": "Website Group", "docname": group}) + + webnotes.bean({ + "doctype": "Website Group", + "group_name": group + "-" + new_group, + "group_title": new_group, + "parent_website_sitemap": parent_website_sitemap, + "group_type": group_type, + "public_read": int(public_read), + "public_write": int(public_write) + }).insert(ignore_permissions=True) \ No newline at end of file diff --git a/webnotes/templates/website_group/tasks.html b/webnotes/templates/website_group/tasks.html index e69de29bb2..7ac084dd5c 100644 --- a/webnotes/templates/website_group/tasks.html +++ b/webnotes/templates/website_group/tasks.html @@ -0,0 +1,32 @@ +
+
+{{ post_list_html }} +
+
+ +
+ + diff --git a/webnotes/templates/website_group/tasks.py b/webnotes/templates/website_group/tasks.py new file mode 100644 index 0000000000..36c96dceef --- /dev/null +++ b/webnotes/templates/website_group/tasks.py @@ -0,0 +1,126 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import webnotes +from webnotes.utils import now_datetime, get_datetime_str +from webnotes.webutils import get_access +from webnotes.templates.website_group.settings import get_settings_context +from webnotes.templates.website_group.post import get_post_context + +def get_views(): + return views + +def get_context(group_context): + tasks_context = {} + + if group_context.view.name in ("open", "closed"): + tasks_context["post_list_html"] = get_post_list_html(group_context["group"]["name"], group_context["view"]) + + elif group_context.view.name == "edit": + post = webnotes.doc("Post", webnotes.form_dict.name).fields + tasks_context["session_user"] = webnotes.session.user + tasks_context["post"] = post + if post.assigned_to: + tasks_context["profile"] = webnotes.doc("Profile", post.assigned_to) + + elif group_context.view.name == "settings": + tasks_context.update(get_settings_context(group_context)) + + elif group_context.view.name == "post": + tasks_context.update(get_post_context(group_context)) + + return tasks_context + +@webnotes.whitelist(allow_guest=True) +def get_post_list_html(group, view, limit_start=0, limit_length=20, status="Open"): + access = get_access(group) + + if isinstance(view, basestring): + view = get_views()[view] + + view = webnotes._dict(view) + + # verify permission for paging + if webnotes.local.form_dict.cmd == "get_post_list_html": + if not access.get("read"): + return webnotes.PermissionError + + if view.name=="open": + now = get_datetime_str(now_datetime()) + order_by = "(p.upvotes + post_reply_count - (timestampdiff(hour, p.creation, \"{}\") / 2)) desc, p.creation desc".format(now) + else: + status = "Closed" + order_by = "p.creation desc" + + posts = webnotes.conn.sql("""select p.*, pr.user_image, pr.first_name, pr.last_name, + (select count(pc.name) from `tabPost` pc where pc.parent_post=p.name) as post_reply_count + from `tabPost` p, `tabProfile` pr + where p.website_group = %s and pr.name = p.owner and ifnull(p.parent_post, '')='' + and p.is_task=1 and p.status=%s + order by {order_by} limit %s, %s""".format(order_by=order_by), + (group, status, int(limit_start), int(limit_length)), as_dict=True) + + context = {"posts": posts, "limit_start": limit_start, "view": view} + + return webnotes.get_template("templates/includes/post_list.html").render(context) + +views = { + "open": { + "name": "open", + "template_path": "templates/website_group/tasks.html", + "url": "/{group}", + "label": "Open", + "icon": "icon-inbox", + "default": True, + "upvote": True, + "idx": 1 + }, + "closed": { + "name": "closed", + "template_path": "templates/website_group/tasks.html", + "url": "/{group}?view=closed", + "label": "Closed", + "icon": "icon-smile", + "idx": 2 + }, + "post": { + "name": "post", + "template_path": "templates/website_group/post.html", + "url": "/{group}?view=post&name={post}", + "label": "Post", + "icon": "icon-comments", + "hidden": True, + "no_cache": True, + "upvote": True, + "idx": 3 + }, + "edit": { + "name": "edit", + "template_path": "templates/website_group/edit_post.html", + "url": "/{group}?view=edit&name={post}", + "label": "Edit Post", + "icon": "icon-pencil", + "hidden": True, + "no_cache": True, + "idx": 4 + }, + "add": { + "name": "add", + "template_path": "templates/website_group/edit_post.html", + "url": "/{group}?view=add", + "label": "Add Post", + "icon": "icon-plus", + "hidden": True, + "idx": 5 + }, + "settings": { + "name": "settings", + "template_path": "templates/website_group/settings.html", + "url": "/{group}?view=settings", + "label": "Settings", + "icon": "icon-cog", + "hidden": True, + "idx": 6 + } +} \ No newline at end of file diff --git a/webnotes/website/css/website.css b/webnotes/website/css/website.css index bb51a28bc8..d9ba145655 100644 --- a/webnotes/website/css/website.css +++ b/webnotes/website/css/website.css @@ -1,4 +1,4 @@ -@media (min-width: 992px) { +@media (min-width: 768px) { .login-wrapper { border-right: 1px solid #f2f2f2; } @@ -163,6 +163,8 @@ img { .navbar-brand { padding-right: 30px; max-width: 80%; + min-height: 20px; + height: auto; } @media (min-width: 768px) { @@ -283,6 +285,7 @@ body { .post .media-object { border-radius: 4px; + max-width: 50px; } .post .media-heading { @@ -298,4 +301,12 @@ body { padding-left: 15px; background-color: #f8f8f8; margin-top: 0px; +} + +textarea { + resize: vertical; +} + +.post-add-textarea { + height: 200px !important; } \ No newline at end of file diff --git a/webnotes/website/doctype/post/post.py b/webnotes/website/doctype/post/post.py index 7db277da8a..86863330e3 100644 --- a/webnotes/website/doctype/post/post.py +++ b/webnotes/website/doctype/post/post.py @@ -9,11 +9,13 @@ import webnotes from webnotes.utils import get_fullname from webnotes.utils.email_lib.bulk import send from webnotes.utils.email_lib import sendmail +from webnotes.utils.file_manager import save_file + +from webnotes.webutils import get_access # TODO move these functions to framework -from aapkamanch.helpers import get_access -from aapkamanch.post import clear_post_cache -from aapkamanch.unit import clear_unit_views +# from aapkamanch.post import clear_post_cache +# from aapkamanch.unit import clear_unit_views class DocType: def __init__(self, d, dl): @@ -42,8 +44,8 @@ class DocType: self.doc.event_datetime = None def on_update(self): - clear_unit_views(self.doc.unit) - clear_post_cache(self.doc.parent_post or self.doc.name) + # clear_unit_views(self.doc.website_group) + # clear_post_cache(self.doc.parent_post or self.doc.name) if self.doc.assigned_to and self.doc.assigned_to != self.assigned_to \ and webnotes.session.user != self.doc.assigned_to: @@ -99,4 +101,102 @@ class DocType: message += "

Click here to view the post

".format(fullname=owner_fullname, post_name=post_name) return message - \ No newline at end of file + +@webnotes.whitelist(allow_guest=True) +def add_post(group, content, picture, picture_name, title=None, parent_post=None, + assigned_to=None, status=None, event_datetime=None): + + access = get_access(group) + if not access.get("write"): + raise webnotes.PermissionError + + if parent_post: + if webnotes.conn.get_value("Post", parent_post, "parent_post"): + webnotes.throw("Cannot reply to a reply") + + group = webnotes.doc("Website Group", group) + post = webnotes.bean({ + "doctype":"Post", + "title": (title or "").title(), + "content": content, + "website_group": group.name, + "parent_post": parent_post or None + }) + + if not parent_post: + if group.group_type == "Tasks": + post.doc.is_task = 1 + post.doc.assigned_to = assigned_to + elif group.group_type == "Events": + post.doc.is_event = 1 + post.doc.event_datetime = event_datetime + + post.ignore_permissions = True + post.insert() + + if picture_name and picture: + process_picture(post, picture_name, picture) + + # send email + if parent_post: + post.run_method("send_email_on_reply") + + return post.doc.parent_post or post.doc.name + +@webnotes.whitelist(allow_guest=True) +def save_post(post, content, picture=None, picture_name=None, title=None, + assigned_to=None, status=None, event_datetime=None): + + post = webnotes.bean("Post", post) + + access = get_access(post.doc.website_group) + if not access.get("write"): + raise webnotes.PermissionError + + # TODO improve error message + if webnotes.session.user != post.doc.owner: + for fieldname in ("title", "content"): + if post.doc.fields.get(fieldname) != locals().get(fieldname): + webnotes.throw("You cannot change: {}".format(fieldname.title())) + + if picture and picture_name: + webnotes.throw("You cannot change: Picture") + + post.doc.fields.update({ + "title": (title or "").title(), + "content": content, + "assigned_to": assigned_to, + "status": status, + "event_datetime": event_datetime + }) + post.ignore_permissions = True + post.save() + + if picture_name and picture: + process_picture(post, picture_name, picture) + + return post.doc.parent_post or post.doc.name + +def process_picture(post, picture_name, picture): + file_data = save_file(picture_name, picture, "Post", post.doc.name, decode=True) + post.doc.picture_url = file_data.file_name or file_data.file_url + webnotes.conn.set_value("Post", post.doc.name, "picture_url", post.doc.picture_url) + # clear_unit_views(post.doc.website_group) + +@webnotes.whitelist() +def suggest_user(group, term): + """suggest a user that has read permission in this group tree""" + profiles = webnotes.conn.sql("""select + pr.name, pr.first_name, pr.last_name, + pr.user_image, pr.fb_location, pr.fb_hometown + from `tabProfile` pr + where (pr.first_name like %(term)s or pr.last_name like %(term)s) + and pr.name not in ("Guest", "Administrator") is not null and pr.enabled=1""", + {"term": "%{}%".format(term), "group": group}, as_dict=True) + + template = webnotes.get_template("templates/includes/profile_display.html") + return [{ + "value": "{} {}".format(pr.first_name or "", pr.last_name or "").strip(), + "profile_html": template.render({"profile": pr}), + "profile": pr.name + } for pr in profiles] diff --git a/webnotes/website/doctype/user_vote/user_vote.py b/webnotes/website/doctype/user_vote/user_vote.py index 9e84e41e86..fb11cea746 100644 --- a/webnotes/website/doctype/user_vote/user_vote.py +++ b/webnotes/website/doctype/user_vote/user_vote.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import webnotes +from webnotes.webutils import get_access class DocType: def __init__(self, d, dl): @@ -31,3 +32,22 @@ class DocType: def on_doctype_update(): webnotes.conn.add_index("User Vote", ["ref_doctype", "ref_name"]) + +# don't allow guest to give vote +@webnotes.whitelist() +def set_vote(ref_doctype, ref_name): + website_group = webnotes.conn.get_value(ref_doctype, ref_name, "website_group") + if not get_access(website_group).get("read"): + raise webnotes.PermissionError + + try: + user_vote = webnotes.bean({ + "doctype": "User Vote", + "ref_doctype": ref_doctype, + "ref_name": ref_name + }) + user_vote.ignore_permissions = True + user_vote.insert() + return "ok" + except webnotes.DuplicateEntryError: + return "duplicate" diff --git a/webnotes/website/doctype/website_group/website_group.py b/webnotes/website/doctype/website_group/website_group.py index 01ee4e4cd3..779f499913 100644 --- a/webnotes/website/doctype/website_group/website_group.py +++ b/webnotes/website/doctype/website_group/website_group.py @@ -27,3 +27,6 @@ class DocType(WebsiteGenerator): if not self.doc.page_name: webnotes.throw(_("Page Name is mandatory"), raise_exception=webnotes.MandatoryError) + + def get_page_title(self): + return self.doc.group_title diff --git a/webnotes/website/doctype/website_settings/website_settings.txt b/webnotes/website/doctype/website_settings/website_settings.txt index 9741d17ee3..68c00e7fab 100644 --- a/webnotes/website/doctype/website_settings/website_settings.txt +++ b/webnotes/website/doctype/website_settings/website_settings.txt @@ -2,7 +2,7 @@ { "creation": "2013-04-30 12:58:46", "docstatus": 0, - "modified": "2013-12-27 16:37:52", + "modified": "2014-02-03 15:25:54", "modified_by": "Administrator", "owner": "Administrator" }, @@ -25,6 +25,8 @@ "permlevel": 0 }, { + "cancel": 0, + "delete": 0, "doctype": "DocPerm", "name": "__common__", "parent": "Website Settings", @@ -48,8 +50,9 @@ "description": "Link that is the website home page. Standard Links (index, login, products, blog, about, contact)", "doctype": "DocField", "fieldname": "home_page", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Home Page", + "options": "Website Sitemap", "reqd": 0 }, { @@ -241,7 +244,6 @@ }, { "amend": 0, - "cancel": 0, "create": 0, "doctype": "DocPerm", "permlevel": 1, diff --git a/webnotes/website/doctype/website_sitemap/website_sitemap.py b/webnotes/website/doctype/website_sitemap/website_sitemap.py index ff171a1f88..d55272833f 100644 --- a/webnotes/website/doctype/website_sitemap/website_sitemap.py +++ b/webnotes/website/doctype/website_sitemap/website_sitemap.py @@ -8,7 +8,7 @@ import webnotes from webnotes.utils.nestedset import DocTypeNestedSet sitemap_fields = ("page_name", "ref_doctype", "docname", "page_or_generator", - "lastmod", "parent_website_sitemap", "public_read", "public_write") + "lastmod", "parent_website_sitemap", "public_read", "public_write", "page_title") class DocType(DocTypeNestedSet): def __init__(self, d, dl): diff --git a/webnotes/website/doctype/website_sitemap_permission/website_sitemap_permission.py b/webnotes/website/doctype/website_sitemap_permission/website_sitemap_permission.py index 6982d9478e..5de997cfcb 100644 --- a/webnotes/website/doctype/website_sitemap_permission/website_sitemap_permission.py +++ b/webnotes/website/doctype/website_sitemap_permission/website_sitemap_permission.py @@ -10,6 +10,7 @@ class DocType: def on_update(self): remove_empty_permissions() + clear_permissions(self.doc.profile) def remove_empty_permissions(): permissions_cache_to_be_cleared = webnotes.conn.sql_list("""select distinct profile @@ -21,20 +22,20 @@ def remove_empty_permissions(): clear_permissions(permissions_cache_to_be_cleared) -def get_access(website_node, profile=None): +def get_access(sitemap_page, profile=None): profile = profile or webnotes.session.user key = "website_sitemap_permissions:{}".format(profile) cache = webnotes.cache() permissions = cache.get_value(key) or {} - if not permissions.get(website_node): - permissions[website_node] = _get_access(website_node, profile) + if not permissions.get(sitemap_page): + permissions[sitemap_page] = _get_access(sitemap_page, profile) cache.set_value(key, permissions) - return permissions.get(website_node) + return permissions.get(sitemap_page) -def _get_access(website_node, profile): - lft, rgt, public_read, public_write = webnotes.conn.get_value("Website Sitemap", website_node, +def _get_access(sitemap_page, profile): + lft, rgt, public_read, public_write = webnotes.conn.get_value("Website Sitemap", sitemap_page, ["lft", "rgt", "public_read", "public_write"]) if not (lft and rgt): @@ -52,7 +53,7 @@ def _get_access(website_node, profile): for perm in webnotes.conn.sql("""select wsp.`read`, wsp.`write`, wsp.`admin`, ws.lft, ws.rgt, ws.name - from `tabWebsite Sitemap Permission` up, `tabWebsite Sitemap` ws + from `tabWebsite Sitemap Permission` wsp, `tabWebsite Sitemap` ws where wsp.profile = %s and wsp.website_sitemap = ws.name order by lft asc""", (profile,), as_dict=True): if perm.lft <= lft and perm.rgt >= rgt: diff --git a/webnotes/website/js/jquery.canvasResize.js b/webnotes/website/js/jquery.canvasResize.js new file mode 100644 index 0000000000..0f8a352b7b --- /dev/null +++ b/webnotes/website/js/jquery.canvasResize.js @@ -0,0 +1,313 @@ +/* + * jQuery canvasResize plugin + * + * Version: 1.2.0 + * Date (d/m/y): 02/10/12 + * Update (d/m/y): 14/05/13 + * Original author: @gokercebeci + * Licensed under the MIT license + * - This plugin working with jquery.exif.js + * (It's under the MPL License http://www.nihilogic.dk/licenses/mpl-license.txt) + * Demo: http://ios6-image-resize.gokercebeci.com/ + * + * - I fixed iOS6 Safari's image file rendering issue for large size image (over mega-pixel) + * using few functions from https://github.com/stomita/ios-imagefile-megapixel + * (detectSubsampling, ) + * And fixed orientation issue by edited http://blog.nihilogic.dk/2008/05/jquery-exif-data-plugin.html + * Thanks, Shinichi Tomita and Jacob Seidelin + */ + +(function($) { + var pluginName = 'canvasResize', + methods = { + newsize: function(w, h, W, H, C) { + var c = C ? 'h' : ''; + if ((W && w > W) || (H && h > H)) { + var r = w / h; + if ((r >= 1 || H === 0) && W && !C) { + w = W; + h = (W / r) >> 0; + } else if (C && r <= (W / H)) { + w = W; + h = (W / r) >> 0; + c = 'w'; + } else { + w = (H * r) >> 0; + h = H; + } + } + return { + 'width': w, + 'height': h, + 'cropped': c + }; + }, + dataURLtoBlob: function(data) { + var mimeString = data.split(',')[0].split(':')[1].split(';')[0]; + var byteString = atob(data.split(',')[1]); + var ab = new ArrayBuffer(byteString.length); + var ia = new Uint8Array(ab); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + var bb = (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder); + if (bb) { + // console.log('BlobBuilder'); + bb = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)(); + bb.append(ab); + return bb.getBlob(mimeString); + } else { + // console.log('Blob'); + bb = new Blob([ab], { + 'type': (mimeString) + }); + return bb; + } + }, + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be subsampled in rendering. + */ + detectSubsampling: function(img) { + var iw = img.width, ih = img.height; + if (iw * ih > 1048576) { // subsampling may happen over megapixel image + var canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + var ctx = canvas.getContext('2d'); + ctx.drawImage(img, -iw + 1, 0); + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering edge pixel or not. + // if alpha value is 0 image is not covering, hence subsampled. + return ctx.getImageData(0, 0, 1, 1).data[3] === 0; + } else { + return false; + } + }, + /** + * Update the orientation according to the specified rotation angle + */ + rotate: function(orientation, angle) { + var o = { + // nothing + 1: {90: 6, 180: 3, 270: 8}, + // horizontal flip + 2: {90: 7, 180: 4, 270: 5}, + // 180 rotate left + 3: {90: 8, 180: 1, 270: 6}, + // vertical flip + 4: {90: 5, 180: 2, 270: 7}, + // vertical flip + 90 rotate right + 5: {90: 2, 180: 7, 270: 4}, + // 90 rotate right + 6: {90: 3, 180: 8, 270: 1}, + // horizontal flip + 90 rotate right + 7: {90: 4, 180: 5, 270: 2}, + // 90 rotate left + 8: {90: 1, 180: 6, 270: 3} + }; + return o[orientation][angle] ? o[orientation][angle] : orientation; + }, + /** + * Transform canvas coordination according to specified frame size and orientation + * Orientation value is from EXIF tag + */ + transformCoordinate: function(canvas, width, height, orientation) { + //console.log(width, height); + switch (orientation) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + default: + canvas.width = width; + canvas.height = height; + } + var ctx = canvas.getContext('2d'); + switch (orientation) { + case 1: + // nothing + break; + case 2: + // horizontal flip + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + case 3: + // 180 rotate left + ctx.translate(width, height); + ctx.rotate(Math.PI); + break; + case 4: + // vertical flip + ctx.translate(0, height); + ctx.scale(1, -1); + break; + case 5: + // vertical flip + 90 rotate right + ctx.rotate(0.5 * Math.PI); + ctx.scale(1, -1); + break; + case 6: + // 90 rotate right + ctx.rotate(0.5 * Math.PI); + ctx.translate(0, -height); + break; + case 7: + // horizontal flip + 90 rotate right + ctx.rotate(0.5 * Math.PI); + ctx.translate(width, -height); + ctx.scale(-1, 1); + break; + case 8: + // 90 rotate left + ctx.rotate(-0.5 * Math.PI); + ctx.translate(-width, 0); + break; + default: + break; + } + }, + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into canvas for some images. + */ + detectVerticalSquash: function(img, iw, ih) { + var canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = ih; + var ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + var data = ctx.getImageData(0, 0, 1, ih).data; + // search image edge pixel position in case it is squashed vertically. + var sy = 0; + var ey = ih; + var py = ih; + while (py > sy) { + var alpha = data[(py - 1) * 4 + 3]; + if (alpha === 0) { + ey = py; + } else { + sy = py; + } + py = (ey + sy) >> 1; + } + var ratio = py / ih; + return ratio === 0 ? 1 : ratio; + }, + callback: function(d) { + return d; + } + }, + defaults = { + width: 300, + height: 0, + crop: false, + quality: 80, + 'callback': methods.callback + }; + function Plugin(file, options) { + this.file = file; + this.options = $.extend({}, defaults, options); + this._defaults = defaults; + this._name = pluginName; + this.init(); + } + Plugin.prototype = { + init: function() { + //this.options.init(this); + var $this = this; + var file = this.file; + + var reader = new FileReader(); + reader.onloadend = function(e) { + var dataURL = e.target.result; + var img = new Image(); + img.onload = function(e) { + // Read Orientation Data in EXIF + $(img).exifLoadFromDataURL(function() { + var orientation = $(img).exif('Orientation')[0] || 1; + orientation = methods.rotate(orientation, $this.options.rotate); + + // CW or CCW ? replace width and height + var size = (orientation >= 5 && orientation <= 8) + ? methods.newsize(img.height, img.width, $this.options.width, $this.options.height, $this.options.crop) + : methods.newsize(img.width, img.height, $this.options.width, $this.options.height, $this.options.crop); + + var iw = img.width, ih = img.height; + var width = size.width, height = size.height; + + //console.log(iw, ih, size.width, size.height, orientation); + + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + ctx.save(); + methods.transformCoordinate(canvas, width, height, orientation); + + // over image size + if (methods.detectSubsampling(img)) { + iw /= 2; + ih /= 2; + } + var d = 1024; // size of tiling canvas + var tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + var tmpCtx = tmpCanvas.getContext('2d'); + var vertSquashRatio = methods.detectVerticalSquash(img, iw, ih); + var sy = 0; + while (sy < ih) { + var sh = sy + d > ih ? ih - sy : d; + var sx = 0; + while (sx < iw) { + var sw = sx + d > iw ? iw - sx : d; + tmpCtx.clearRect(0, 0, d, d); + tmpCtx.drawImage(img, -sx, -sy); + var dx = Math.floor(sx * width / iw); + var dw = Math.ceil(sw * width / iw); + var dy = Math.floor(sy * height / ih / vertSquashRatio); + var dh = Math.ceil(sh * height / ih / vertSquashRatio); + ctx.drawImage(tmpCanvas, 0, 0, sw, sh, dx, dy, dw, dh); + sx += d; + } + sy += d; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + + // if cropped or rotated width and height data replacing issue + var newcanvas = document.createElement('canvas'); + newcanvas.width = size.cropped === 'h' ? height : width; + newcanvas.height = size.cropped === 'w' ? width : height; + var x = size.cropped === 'h' ? (height - width) * .5 : 0; + var y = size.cropped === 'w' ? (width - height) * .5 : 0; + newctx = newcanvas.getContext('2d'); + newctx.drawImage(canvas, x, y, width, height); + + if (file.type === "image/png") { + var data = newcanvas.toDataURL(file.type); + } else { + var data = newcanvas.toDataURL("image/jpeg", ($this.options.quality * .01)); + } + + // CALLBACK + $this.options.callback(data, width, height); + + }); + }; + img.src = dataURL; + // ===================================================== + }; + reader.readAsDataURL(file); + + } + }; + $[pluginName] = function(file, options) { + if (typeof file === 'string') + return methods[file](options); + else + new Plugin(file, options); + }; + +})(jQuery); \ No newline at end of file diff --git a/webnotes/website/js/jquery.exif.js b/webnotes/website/js/jquery.exif.js new file mode 100644 index 0000000000..15b891bf3c --- /dev/null +++ b/webnotes/website/js/jquery.exif.js @@ -0,0 +1,957 @@ + +/* + * Javascript EXIF Reader - jQuery plugin 0.1.3 + * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/ + * Licensed under the MPL License [http://www.nihilogic.dk/licenses/mpl-license.txt] + */ + +/* + * I added three functions for read EXIF from dataURL + * - getImageDataFromDataURL + * - getDataFromDataURL + * - jQuery.fn.exifLoadFromDataURL + * + * http://orientation.gokercebeci.com + * @gokercebeci + */ + +(function() { + + + var BinaryFile = function(strData, iDataOffset, iDataLength) { + var data = strData; + var dataOffset = iDataOffset || 0; + var dataLength = 0; + + this.getRawData = function() { + return data; + } + + if (typeof strData == "string") { + dataLength = iDataLength || data.length; + + this.getByteAt = function(iOffset) { + return data.charCodeAt(iOffset + dataOffset) & 0xFF; + } + } else if (typeof strData == "unknown") { + dataLength = iDataLength || IEBinary_getLength(data); + + this.getByteAt = function(iOffset) { + return IEBinary_getByteAt(data, iOffset + dataOffset); + } + } + + this.getLength = function() { + return dataLength; + } + + this.getSByteAt = function(iOffset) { + var iByte = this.getByteAt(iOffset); + if (iByte > 127) + return iByte - 256; + else + return iByte; + } + + this.getShortAt = function(iOffset, bBigEndian) { + var iShort = bBigEndian ? + (this.getByteAt(iOffset) << 8) + this.getByteAt(iOffset + 1) + : (this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset) + if (iShort < 0) + iShort += 65536; + return iShort; + } + this.getSShortAt = function(iOffset, bBigEndian) { + var iUShort = this.getShortAt(iOffset, bBigEndian); + if (iUShort > 32767) + return iUShort - 65536; + else + return iUShort; + } + this.getLongAt = function(iOffset, bBigEndian) { + var iByte1 = this.getByteAt(iOffset), + iByte2 = this.getByteAt(iOffset + 1), + iByte3 = this.getByteAt(iOffset + 2), + iByte4 = this.getByteAt(iOffset + 3); + + var iLong = bBigEndian ? + (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4 + : (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1; + if (iLong < 0) + iLong += 4294967296; + return iLong; + } + this.getSLongAt = function(iOffset, bBigEndian) { + var iULong = this.getLongAt(iOffset, bBigEndian); + if (iULong > 2147483647) + return iULong - 4294967296; + else + return iULong; + } + this.getStringAt = function(iOffset, iLength) { + var aStr = []; + for (var i = iOffset, j = 0; i < iOffset + iLength; i++, j++) { + aStr[j] = String.fromCharCode(this.getByteAt(i)); + } + return aStr.join(""); + } + + this.getCharAt = function(iOffset) { + return String.fromCharCode(this.getByteAt(iOffset)); + } + this.toBase64 = function() { + return window.btoa(data); + } + this.fromBase64 = function(strBase64) { + data = window.atob(strBase64); + } + } + + + var BinaryAjax = (function() { + + function createRequest() { + var oHTTP = null; + if (window.XMLHttpRequest) { + oHTTP = new XMLHttpRequest(); + } else if (window.ActiveXObject) { + oHTTP = new ActiveXObject("Microsoft.XMLHTTP"); + } + return oHTTP; + } + + function getHead(strURL, fncCallback, fncError) { + var oHTTP = createRequest(); + if (oHTTP) { + if (fncCallback) { + if (typeof(oHTTP.onload) != "undefined") { + oHTTP.onload = function() { + if (oHTTP.status == "200") { + fncCallback(this); + } else { + if (fncError) + fncError(); + } + oHTTP = null; + }; + } else { + oHTTP.onreadystatechange = function() { + if (oHTTP.readyState == 4) { + if (oHTTP.status == "200") { + fncCallback(this); + } else { + if (fncError) + fncError(); + } + oHTTP = null; + } + }; + } + } + oHTTP.open("HEAD", strURL, true); + oHTTP.send(null); + } else { + if (fncError) + fncError(); + } + } + + function sendRequest(strURL, fncCallback, fncError, aRange, bAcceptRanges, iFileSize) { + var oHTTP = createRequest(); + if (oHTTP) { + + var iDataOffset = 0; + if (aRange && !bAcceptRanges) { + iDataOffset = aRange[0]; + } + var iDataLen = 0; + if (aRange) { + iDataLen = aRange[1] - aRange[0] + 1; + } + + if (fncCallback) { + if (typeof(oHTTP.onload) != "undefined") { + oHTTP.onload = function() { + + if (oHTTP.status == "200" || oHTTP.status == "206" || oHTTP.status == "0") { + this.binaryResponse = new BinaryFile(this.responseText, iDataOffset, iDataLen); + this.fileSize = iFileSize || this.getResponseHeader("Content-Length"); + fncCallback(this); + } else { + if (fncError) + fncError(); + } + oHTTP = null; + }; + } else { + oHTTP.onreadystatechange = function() { + if (oHTTP.readyState == 4) { + if (oHTTP.status == "200" || oHTTP.status == "206" || oHTTP.status == "0") { + this.binaryResponse = new BinaryFile(oHTTP.responseBody, iDataOffset, iDataLen); + this.fileSize = iFileSize || this.getResponseHeader("Content-Length"); + fncCallback(this); + } else { + if (fncError) + fncError(); + } + oHTTP = null; + } + }; + } + } + oHTTP.open("GET", strURL, true); + + if (oHTTP.overrideMimeType) + oHTTP.overrideMimeType('text/plain; charset=x-user-defined'); + + if (aRange && bAcceptRanges) { + oHTTP.setRequestHeader("Range", "bytes=" + aRange[0] + "-" + aRange[1]); + } + + oHTTP.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 1970 00:00:00 GMT"); + + oHTTP.send(null); + } else { + if (fncError) + fncError(); + } + } + + return function(strURL, fncCallback, fncError, aRange) { + + if (aRange) { + getHead( + strURL, + function(oHTTP) { + var iLength = parseInt(oHTTP.getResponseHeader("Content-Length"), 10); + var strAcceptRanges = oHTTP.getResponseHeader("Accept-Ranges"); + + var iStart, iEnd; + iStart = aRange[0]; + if (aRange[0] < 0) + iStart += iLength; + iEnd = iStart + aRange[1] - 1; + + sendRequest(strURL, fncCallback, fncError, [iStart, iEnd], (strAcceptRanges == "bytes"), iLength); + } + ); + + } else { + sendRequest(strURL, fncCallback, fncError); + } + } + + }()); + + var script = document.createElement("script"); + script.type = 'text/vbscript'; + script.innerHTML = + "Function IEBinary_getByteAt(strBinary, iOffset)\r\n" + + " IEBinary_getByteAt = AscB(MidB(strBinary,iOffset+1,1))\r\n" + + "End Function\r\n" + + "Function IEBinary_getLength(strBinary)\r\n" + + " IEBinary_getLength = LenB(strBinary)\r\n" + + "End Function"; + document.head.appendChild(script); + + var EXIF = {}; + + (function() { + + var bDebug = false; + + EXIF.Tags = { + + // version tags + 0x9000: "ExifVersion", // EXIF version + 0xA000: "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001: "ColorSpace", // Color space information tag + + // image configuration + 0xA002: "PixelXDimension", // Valid width of meaningful image + 0xA003: "PixelYDimension", // Valid height of meaningful image + 0x9101: "ComponentsConfiguration", // Information about channels + 0x9102: "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C: "MakerNote", // Any desired information written by the manufacturer + 0x9286: "UserComment", // Comments by user + + // related file + 0xA004: "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003: "DateTimeOriginal", // Date and time when the original image was generated + 0x9004: "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290: "SubsecTime", // Fractions of seconds for DateTime + 0x9291: "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292: "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A: "ExposureTime", // Exposure time (in seconds) + 0x829D: "FNumber", // F number + 0x8822: "ExposureProgram", // Exposure program + 0x8824: "SpectralSensitivity", // Spectral sensitivity + 0x8827: "ISOSpeedRatings", // ISO speed rating + 0x8828: "OECF", // Optoelectric conversion factor + 0x9201: "ShutterSpeedValue", // Shutter speed + 0x9202: "ApertureValue", // Lens aperture + 0x9203: "BrightnessValue", // Value of brightness + 0x9204: "ExposureBias", // Exposure bias + 0x9205: "MaxApertureValue", // Smallest F number of lens + 0x9206: "SubjectDistance", // Distance to subject in meters + 0x9207: "MeteringMode", // Metering mode + 0x9208: "LightSource", // Kind of light source + 0x9209: "Flash", // Flash status + 0x9214: "SubjectArea", // Location and area of main subject + 0x920A: "FocalLength", // Focal length of the lens in mm + 0xA20B: "FlashEnergy", // Strobe energy in BCPS + 0xA20C: "SpatialFrequencyResponse", // + 0xA20E: "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F: "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210: "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214: "SubjectLocation", // Location of subject in image + 0xA215: "ExposureIndex", // Exposure index selected on camera + 0xA217: "SensingMethod", // Image sensor type + 0xA300: "FileSource", // Image source (3 == DSC) + 0xA301: "SceneType", // Scene type (1 == directly photographed) + 0xA302: "CFAPattern", // Color filter array geometric pattern + 0xA401: "CustomRendered", // Special processing + 0xA402: "ExposureMode", // Exposure mode + 0xA403: "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404: "DigitalZoomRation", // Digital zoom ratio + 0xA405: "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406: "SceneCaptureType", // Type of scene + 0xA407: "GainControl", // Degree of overall image gain adjustment + 0xA408: "Contrast", // Direction of contrast processing applied by camera + 0xA409: "Saturation", // Direction of saturation processing applied by camera + 0xA40A: "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B: "DeviceSettingDescription", // + 0xA40C: "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005: "InteroperabilityIFDPointer", + 0xA420: "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + EXIF.TiffTags = { + 0x0100: "ImageWidth", + 0x0101: "ImageHeight", + 0x8769: "ExifIFDPointer", + 0x8825: "GPSInfoIFDPointer", + 0xA005: "InteroperabilityIFDPointer", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x011C: "PlanarConfiguration", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x0128: "ResolutionUnit", + 0x0111: "StripOffsets", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteCounts", + 0x0201: "JPEGInterchangeFormat", + 0x0202: "JPEGInterchangeFormatLength", + 0x012D: "TransferFunction", + 0x013E: "WhitePoint", + 0x013F: "PrimaryChromaticities", + 0x0211: "YCbCrCoefficients", + 0x0214: "ReferenceBlackWhite", + 0x0132: "DateTime", + 0x010E: "ImageDescription", + 0x010F: "Make", + 0x0110: "Model", + 0x0131: "Software", + 0x013B: "Artist", + 0x8298: "Copyright" + } + + EXIF.GPSTags = { + 0x0000: "GPSVersionID", + 0x0001: "GPSLatitudeRef", + 0x0002: "GPSLatitude", + 0x0003: "GPSLongitudeRef", + 0x0004: "GPSLongitude", + 0x0005: "GPSAltitudeRef", + 0x0006: "GPSAltitude", + 0x0007: "GPSTimeStamp", + 0x0008: "GPSSatellites", + 0x0009: "GPSStatus", + 0x000A: "GPSMeasureMode", + 0x000B: "GPSDOP", + 0x000C: "GPSSpeedRef", + 0x000D: "GPSSpeed", + 0x000E: "GPSTrackRef", + 0x000F: "GPSTrack", + 0x0010: "GPSImgDirectionRef", + 0x0011: "GPSImgDirection", + 0x0012: "GPSMapDatum", + 0x0013: "GPSDestLatitudeRef", + 0x0014: "GPSDestLatitude", + 0x0015: "GPSDestLongitudeRef", + 0x0016: "GPSDestLongitude", + 0x0017: "GPSDestBearingRef", + 0x0018: "GPSDestBearing", + 0x0019: "GPSDestDistanceRef", + 0x001A: "GPSDestDistance", + 0x001B: "GPSProcessingMethod", + 0x001C: "GPSAreaInformation", + 0x001D: "GPSDateStamp", + 0x001E: "GPSDifferential" + } + + EXIF.StringValues = { + ExposureProgram: { + 0: "Not defined", + 1: "Manual", + 2: "Normal program", + 3: "Aperture priority", + 4: "Shutter priority", + 5: "Creative program", + 6: "Action program", + 7: "Portrait mode", + 8: "Landscape mode" + }, + MeteringMode: { + 0: "Unknown", + 1: "Average", + 2: "CenterWeightedAverage", + 3: "Spot", + 4: "MultiSpot", + 5: "Pattern", + 6: "Partial", + 255: "Other" + }, + LightSource: { + 0: "Unknown", + 1: "Daylight", + 2: "Fluorescent", + 3: "Tungsten (incandescent light)", + 4: "Flash", + 9: "Fine weather", + 10: "Cloudy weather", + 11: "Shade", + 12: "Daylight fluorescent (D 5700 - 7100K)", + 13: "Day white fluorescent (N 4600 - 5400K)", + 14: "Cool white fluorescent (W 3900 - 4500K)", + 15: "White fluorescent (WW 3200 - 3700K)", + 17: "Standard light A", + 18: "Standard light B", + 19: "Standard light C", + 20: "D55", + 21: "D65", + 22: "D75", + 23: "D50", + 24: "ISO studio tungsten", + 255: "Other" + }, + Flash: { + 0x0000: "Flash did not fire", + 0x0001: "Flash fired", + 0x0005: "Strobe return light not detected", + 0x0007: "Strobe return light detected", + 0x0009: "Flash fired, compulsory flash mode", + 0x000D: "Flash fired, compulsory flash mode, return light not detected", + 0x000F: "Flash fired, compulsory flash mode, return light detected", + 0x0010: "Flash did not fire, compulsory flash mode", + 0x0018: "Flash did not fire, auto mode", + 0x0019: "Flash fired, auto mode", + 0x001D: "Flash fired, auto mode, return light not detected", + 0x001F: "Flash fired, auto mode, return light detected", + 0x0020: "No flash function", + 0x0041: "Flash fired, red-eye reduction mode", + 0x0045: "Flash fired, red-eye reduction mode, return light not detected", + 0x0047: "Flash fired, red-eye reduction mode, return light detected", + 0x0049: "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059: "Flash fired, auto mode, red-eye reduction mode", + 0x005D: "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F: "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod: { + 1: "Not defined", + 2: "One-chip color area sensor", + 3: "Two-chip color area sensor", + 4: "Three-chip color area sensor", + 5: "Color sequential area sensor", + 7: "Trilinear sensor", + 8: "Color sequential linear sensor" + }, + SceneCaptureType: { + 0: "Standard", + 1: "Landscape", + 2: "Portrait", + 3: "Night scene" + }, + SceneType: { + 1: "Directly photographed" + }, + CustomRendered: { + 0: "Normal process", + 1: "Custom process" + }, + WhiteBalance: { + 0: "Auto white balance", + 1: "Manual white balance" + }, + GainControl: { + 0: "None", + 1: "Low gain up", + 2: "High gain up", + 3: "Low gain down", + 4: "High gain down" + }, + Contrast: { + 0: "Normal", + 1: "Soft", + 2: "Hard" + }, + Saturation: { + 0: "Normal", + 1: "Low saturation", + 2: "High saturation" + }, + Sharpness: { + 0: "Normal", + 1: "Soft", + 2: "Hard" + }, + SubjectDistanceRange: { + 0: "Unknown", + 1: "Macro", + 2: "Close view", + 3: "Distant view" + }, + FileSource: { + 3: "DSC" + }, + Components: { + 0: "", + 1: "Y", + 2: "Cb", + 3: "Cr", + 4: "R", + 5: "G", + 6: "B" + } + } + + function addEvent(oElement, strEvent, fncHandler) + { + if (oElement.addEventListener) { + oElement.addEventListener(strEvent, fncHandler, false); + } else if (oElement.attachEvent) { + oElement.attachEvent("on" + strEvent, fncHandler); + } + } + + + function imageHasData(oImg) + { + return !!(oImg.exifdata); + } + + function getImageData(oImg, fncCallback) + { + BinaryAjax( + oImg.src, + function(oHTTP) { + console.log('BINARY', oHTTP.binaryResponse); + var oEXIF = findEXIFinJPEG(oHTTP.binaryResponse); + oImg.exifdata = oEXIF || {}; + if (fncCallback) + fncCallback(); + } + ) + } + + function getImageDataFromDataURL(oImg, fncCallback) + { + var byteString = atob(oImg.src.split(',')[1]); + var f = new BinaryFile(byteString, 0, byteString.length) + var oEXIF = findEXIFinJPEG(f); + oImg.exifdata = oEXIF || {}; + if (fncCallback) + fncCallback(); + } + + function findEXIFinJPEG(oFile) { + var aMarkers = []; + + if (oFile.getByteAt(0) != 0xFF || oFile.getByteAt(1) != 0xD8) { + return false; // not a valid jpeg + } + + var iOffset = 2; + var iLength = oFile.getLength(); + while (iOffset < iLength) { + if (oFile.getByteAt(iOffset) != 0xFF) { + if (bDebug) + console.log("Not a valid marker at offset " + iOffset + ", found: " + oFile.getByteAt(iOffset)); + return false; // not a valid marker, something is wrong + } + + var iMarker = oFile.getByteAt(iOffset + 1); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (iMarker == 22400) { + if (bDebug) + console.log("Found 0xFFE1 marker"); + return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset + 2, true) - 2); + iOffset += 2 + oFile.getShortAt(iOffset + 2, true); + + } else if (iMarker == 225) { + // 0xE1 = Application-specific 1 (for EXIF) + if (bDebug) + console.log("Found 0xFFE1 marker"); + return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset + 2, true) - 2); + + } else { + iOffset += 2 + oFile.getShortAt(iOffset + 2, true); + } + + } + + } + + + function readTags(oFile, iTIFFStart, iDirStart, oStrings, bBigEnd) + { + var iEntries = oFile.getShortAt(iDirStart, bBigEnd); + var oTags = {}; + for (var i = 0; i < iEntries; i++) { + var iEntryOffset = iDirStart + i * 12 + 2; + var strTag = oStrings[oFile.getShortAt(iEntryOffset, bBigEnd)]; + if (!strTag && bDebug) + console.log("Unknown tag: " + oFile.getShortAt(iEntryOffset, bBigEnd)); + oTags[strTag] = readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd); + } + return oTags; + } + + + function readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd) + { + var iType = oFile.getShortAt(iEntryOffset + 2, bBigEnd); + var iNumValues = oFile.getLongAt(iEntryOffset + 4, bBigEnd); + var iValueOffset = oFile.getLongAt(iEntryOffset + 8, bBigEnd) + iTIFFStart; + + switch (iType) { + case 1: // byte, 8-bit unsigned int + case 7: // undefined, 8-bit byte, value depending on field + if (iNumValues == 1) { + return oFile.getByteAt(iEntryOffset + 8, bBigEnd); + } else { + var iValOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8); + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getByteAt(iValOffset + n); + } + return aVals; + } + break; + + case 2: // ascii, 8-bit byte + var iStringOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8); + return oFile.getStringAt(iStringOffset, iNumValues - 1); + break; + + case 3: // short, 16 bit int + if (iNumValues == 1) { + return oFile.getShortAt(iEntryOffset + 8, bBigEnd); + } else { + var iValOffset = iNumValues > 2 ? iValueOffset : (iEntryOffset + 8); + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getShortAt(iValOffset + 2 * n, bBigEnd); + } + return aVals; + } + break; + + case 4: // long, 32 bit int + if (iNumValues == 1) { + return oFile.getLongAt(iEntryOffset + 8, bBigEnd); + } else { + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getLongAt(iValueOffset + 4 * n, bBigEnd); + } + return aVals; + } + break; + case 5: // rational = two long values, first is numerator, second is denominator + if (iNumValues == 1) { + return oFile.getLongAt(iValueOffset, bBigEnd) / oFile.getLongAt(iValueOffset + 4, bBigEnd); + } else { + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getLongAt(iValueOffset + 8 * n, bBigEnd) / oFile.getLongAt(iValueOffset + 4 + 8 * n, bBigEnd); + } + return aVals; + } + break; + case 9: // slong, 32 bit signed int + if (iNumValues == 1) { + return oFile.getSLongAt(iEntryOffset + 8, bBigEnd); + } else { + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getSLongAt(iValueOffset + 4 * n, bBigEnd); + } + return aVals; + } + break; + case 10: // signed rational, two slongs, first is numerator, second is denominator + if (iNumValues == 1) { + return oFile.getSLongAt(iValueOffset, bBigEnd) / oFile.getSLongAt(iValueOffset + 4, bBigEnd); + } else { + var aVals = []; + for (var n = 0; n < iNumValues; n++) { + aVals[n] = oFile.getSLongAt(iValueOffset + 8 * n, bBigEnd) / oFile.getSLongAt(iValueOffset + 4 + 8 * n, bBigEnd); + } + return aVals; + } + break; + } + } + + + function readEXIFData(oFile, iStart, iLength) + { + if (oFile.getStringAt(iStart, 4) != "Exif") { + if (bDebug) + console.log("Not valid EXIF data! " + oFile.getStringAt(iStart, 4)); + return false; + } + + var bBigEnd; + + var iTIFFOffset = iStart + 6; + + // test for TIFF validity and endianness + if (oFile.getShortAt(iTIFFOffset) == 0x4949) { + bBigEnd = false; + } else if (oFile.getShortAt(iTIFFOffset) == 0x4D4D) { + bBigEnd = true; + } else { + if (bDebug) + console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return false; + } + + if (oFile.getShortAt(iTIFFOffset + 2, bBigEnd) != 0x002A) { + if (bDebug) + console.log("Not valid TIFF data! (no 0x002A)"); + return false; + } + + if (oFile.getLongAt(iTIFFOffset + 4, bBigEnd) != 0x00000008) { + if (bDebug) + console.log("Not valid TIFF data! (First offset not 8)", oFile.getShortAt(iTIFFOffset + 4, bBigEnd)); + return false; + } + + var oTags = readTags(oFile, iTIFFOffset, iTIFFOffset + 8, EXIF.TiffTags, bBigEnd); + + if (oTags.ExifIFDPointer) { + var oEXIFTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.ExifIFDPointer, EXIF.Tags, bBigEnd); + for (var strTag in oEXIFTags) { + switch (strTag) { + case "LightSource" : + case "Flash" : + case "MeteringMode" : + case "ExposureProgram" : + case "SensingMethod" : + case "SceneCaptureType" : + case "SceneType" : + case "CustomRendered" : + case "WhiteBalance" : + case "GainControl" : + case "Contrast" : + case "Saturation" : + case "Sharpness" : + case "SubjectDistanceRange" : + case "FileSource" : + oEXIFTags[strTag] = EXIF.StringValues[strTag][oEXIFTags[strTag]]; + break; + + case "ExifVersion" : + case "FlashpixVersion" : + oEXIFTags[strTag] = String.fromCharCode(oEXIFTags[strTag][0], oEXIFTags[strTag][1], oEXIFTags[strTag][2], oEXIFTags[strTag][3]); + break; + + case "ComponentsConfiguration" : + oEXIFTags[strTag] = + EXIF.StringValues.Components[oEXIFTags[strTag][0]] + + EXIF.StringValues.Components[oEXIFTags[strTag][1]] + + EXIF.StringValues.Components[oEXIFTags[strTag][2]] + + EXIF.StringValues.Components[oEXIFTags[strTag][3]]; + break; + } + oTags[strTag] = oEXIFTags[strTag]; + } + } + + if (oTags.GPSInfoIFDPointer) { + var oGPSTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.GPSInfoIFDPointer, EXIF.GPSTags, bBigEnd); + for (var strTag in oGPSTags) { + switch (strTag) { + case "GPSVersionID" : + oGPSTags[strTag] = oGPSTags[strTag][0] + + "." + oGPSTags[strTag][1] + + "." + oGPSTags[strTag][2] + + "." + oGPSTags[strTag][3]; + break; + } + oTags[strTag] = oGPSTags[strTag]; + } + } + + return oTags; + } + + + EXIF.getData = function(oImg, fncCallback) + { + if (!oImg.complete) + return false; + if (!imageHasData(oImg)) { + getImageData(oImg, fncCallback); + } else { + if (fncCallback) + fncCallback(); + } + return true; + } + + EXIF.getDataFromDataURL = function(oImg, fncCallback) + { + if (!oImg.complete) + return false; + if (!imageHasData(oImg)) { + getImageDataFromDataURL(oImg, fncCallback); + } else { + if (fncCallback) + fncCallback(); + } + return true; + } + + EXIF.getTag = function(oImg, strTag) + { + if (!imageHasData(oImg)) + return; + return oImg.exifdata[strTag]; + } + + EXIF.getAllTags = function(oImg) + { + if (!imageHasData(oImg)) + return {}; + var oData = oImg.exifdata; + var oAllTags = {}; + for (var a in oData) { + if (oData.hasOwnProperty(a)) { + oAllTags[a] = oData[a]; + } + } + return oAllTags; + } + + EXIF.pretty = function(oImg) + { + if (!imageHasData(oImg)) + return ""; + var oData = oImg.exifdata; + var strPretty = ""; + for (var a in oData) { + if (oData.hasOwnProperty(a)) { + if (typeof oData[a] == "object") { + strPretty += a + " : [" + oData[a].length + " values]\r\n"; + } else { + strPretty += a + " : " + oData[a] + "\r\n"; + } + } + } + return strPretty; + } + + EXIF.readFromBinaryFile = function(oFile) { + return findEXIFinJPEG(oFile); + } + + function loadAllImages() + { + var aImages = document.getElementsByTagName("img"); + for (var i = 0; i < aImages.length; i++) { + if (aImages[i].getAttribute("exif") == "true") { + if (!aImages[i].complete) { + addEvent(aImages[i], "load", + function() { + EXIF.getData(this); + } + ); + } else { + EXIF.getData(aImages[i]); + } + } + } + } + + // automatically load exif data for all images with exif=true when doc is ready + jQuery(document).ready(loadAllImages); + + // load data for images manually + jQuery.fn.exifLoad = function(fncCallback) { + return this.each(function() { + EXIF.getData(this, fncCallback) + }); + } + + // load data for images manually + jQuery.fn.exifLoadFromDataURL = function(fncCallback) { + return this.each(function() { + EXIF.getDataFromDataURL(this, fncCallback) + return true; + }); + } + + jQuery.fn.exif = function(strTag) { + var aStrings = []; + this.each(function() { + aStrings.push(EXIF.getTag(this, strTag)); + }); + return aStrings; + } + + jQuery.fn.exifAll = function() { + var aStrings = []; + this.each(function() { + aStrings.push(EXIF.getAllTags(this)); + }); + return aStrings; + } + + jQuery.fn.exifPretty = function() { + var aStrings = []; + this.each(function() { + aStrings.push(EXIF.pretty(this)); + }); + return aStrings; + } + + + })(); + + +})(); diff --git a/webnotes/website/js/website.js b/webnotes/website/js/website.js index 7e098a6dbd..66f2207981 100644 --- a/webnotes/website/js/website.js +++ b/webnotes/website/js/website.js @@ -1,20 +1,9 @@ // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt -if(!window.wn) wn = {}; +// MIT License. See license.txt + +wn.provide("website"); $.extend(wn, { - provide: function(namespace) { - var nsl = namespace.split('.'); - var parent = window; - for(var i=0; i Switch To App'); } + wn.render_user(); + $(document).trigger("page_change"); }); $(document).on("page_change", function() { $(".page-header").toggleClass("hidden", !!!$(".page-header").text().trim()); $(".page-footer").toggleClass("hidden", !!!$(".page-footer").text().trim()); -}); + // add prive pages to sidebar + if(website.private_pages && $(".page-sidebar").length) { + $(data.private_pages).prependTo(".page-sidebar"); + } + + $(document).trigger("apply_permissions"); + wn.datetime.refresh_when(); +}); diff --git a/webnotes/website/js/website_group.js b/webnotes/website/js/website_group.js new file mode 100644 index 0000000000..b14d82df1a --- /dev/null +++ b/webnotes/website/js/website_group.js @@ -0,0 +1,529 @@ +// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +wn.provide("website"); +$.extend(website, { + toggle_permitted: function() { + if(website.access) { + // hide certain views + $('li[data-view="add"]').toggleClass("hide", !website.access.write); + $('li[data-view="settings"]').toggleClass("hide", !website.access.admin); + $('li[data-view="edit"]').toggleClass("hide", website.view!=="edit"); + // $('li[data-view="settings"]').toggleClass("hide", !website.access.admin); + + // show message + $(".post-list-help").html(!website.access.write ? "You do not have permission to post" : ""); + } + }, + setup_pagination: function($btn, opts) { + $btn.removeClass("hide"); + + $btn.on("click", function() { + wn.call($.extend({ + btn: $btn, + type: "GET", + callback: function(data) { + if(opts.prepend) { + opts.$wrapper.prepend(data.message); + } else { + opts.$wrapper.append(data.message); + } + + $btn.toggleClass("hide", !(data.message && data.message.length===opts.args.limit_length)); + } + }, opts)) + }); + }, + bind_add_post: function() { + $(".btn-post-add").on("click", website.add_post); + + $pic_input = $(".control-post-add-picture").on("change", website.add_picture); + $(".btn-post-add-picture").on("click", function() { + $pic_input.click(); + }); + }, + add_post: function() { + if(website.post) { + wn.msgprint("Post already exists. Cannot add again!"); + return; + } + + website._update_post(this, "webnotes.website.doctype.post.post.add_post"); + }, + bind_save_post: function() { + $(".btn-post-add").addClass("hide"); + $(".btn-post-save").removeClass("hide").on("click", website.save_post); + $(".post-picture").toggleClass("hide", !$(".post-picture").attr("src")); + }, + save_post: function() { + if(!website.post) { + wn.msgprint("Post does not exist. Please add post!"); + return; + } + + website._update_post(this, "webnotes.website.doctype.post.post.save_post"); + }, + _update_post: function(btn, cmd) { + var values = website.get_editor_values(); + if(!values) { + return; + } + + wn.call({ + btn: btn, + type: "POST", + args: $.extend({ + cmd: cmd, + group: website.group, + post: website.post || undefined + }, values), + callback: function(data) { + var url = "/" + website.group + "?view=post&name=" + data.message; + window.location.href = url; + + // if(history.pushState) { + // app.get_content(url); + // } else { + // window.location.href = url; + // } + } + }); + }, + get_editor_values: function() { + var values = {}; + $.each($('.post-editor [data-fieldname]'), function(i, ele) { + var $ele = $(ele); + values[$ele.attr("data-fieldname")] = $ele.val(); + }); + + values.parent_post = $(".post-editor").attr("data-parent-post"); + values.picture_name = $(".control-post-add-picture").val() || null; + + var dataurl = $(".post-picture img").attr("src"); + values.picture = dataurl ? dataurl.split(",")[1] : "" + + // validations + if(!values.parent_post && !values.title) { + wn.msgprint("Please enter title!"); + return; + } else if(!values.content) { + wn.msgprint("Please enter some text!"); + return; + } else if($('.post-editor [data-fieldname="event_datetime"]').length && !values.event_datetime) { + wn.msgprint("Please enter Event's Date and Time!"); + return; + } + + // post process + // convert links in content + values.content = website.process_external_links(values.content); + + return values; + }, + process_external_links: function(content) { + return content.replace(/([^\s]*)(http|https|ftp):\/\/[^\s\[\]\(\)]+/g, function(match, p1) { + // mimicing look behind! should not have anything in p1 + // replace(/match/g) + // replace(/(p1)(p2)/g) + // so, when there is a character before http://, it shouldn't be replaced! + if(p1) return match; + + return "["+match+"]("+match+")"; + }); + }, + add_picture: function() { + if (this.type === 'file' && this.files && this.files.length > 0) { + $.each(this.files, function (idx, fileobj) { + if (/^image\//.test(fileobj.type)) { + $.canvasResize(fileobj, { + width: 500, + height: 0, + crop: false, + quality: 80, + callback: function(data, width, height) { + $(".post-picture").removeClass("hide").find("img").attr("src", data); + } + }); + } + }); + } + return false; + }, + setup_tasks_editor: function() { + // assign events + var $post_editor = $(".post-editor"); + var $control_assign = $post_editor.find('.control-assign'); + + var bind_close = function() { + var close = $post_editor.find("a.close") + if(close.length) { + close.on("click", function() { + // clear assignment + $post_editor.find(".assigned-to").addClass("hide"); + $post_editor.find(".assigned-profile").html(""); + $post_editor.find('[data-fieldname="assigned_to"]').val(null); + $control_assign.val(null); + }); + } + } + + if($control_assign.length) { + website.setup_autosuggest({ + $control: $control_assign, + select: function(value, item) { + var $assigned_to = $post_editor.find(".assigned-to").removeClass("hide"); + $assigned_to.find(".assigned-profile").html(item.profile_html); + $post_editor.find('[data-fieldname="assigned_to"]').val(value); + bind_close(); + }, + method: "webnotes.website.doctype.post.post.suggest_user" + }); + bind_close(); + } + }, + setup_event_editor: function() { + var $post_editor = $(".post-editor"); + var $control_event = $post_editor.find('.control-event').empty(); + var $event_field = $post_editor.find('[data-fieldname="event_datetime"]'); + + var set_event = function($control) { + var datetime = website.datetimepicker.obj_to_str($control_event.datepicker("getDate")); + if($event_field.val() !== datetime) { + $event_field.val(datetime); + } + }; + + website.setup_datepicker({ + $control: $control_event, + onClose: function() { set_event($control_event) } + }); + + if($event_field.val()) { + $control_event.val(website.datetimepicker.format_datetime($event_field.val())); + } + }, + format_event_timestamps: function() { + var format = function(datetime) { + if(!datetime) return ""; + var date = datetime.split(" ")[0].split("-"); + var time = datetime.split(" ")[1].split(":"); + var tt = "am"; + if(time[0] >= 12) { + time[0] = parseInt(time[0]) - 12; + tt = "pm"; + } + if(!parseInt(time[0])) { + time[0] = 12; + } + + var hhmm = [time[0], time[1]].join(":") + + return [date[2], date[1], date[0]].join("-") + " " + hhmm + " " + tt; + } + $(".event-timestamp").each(function() { + $(this).html(format($(this).attr("data-timestamp"))); + }) + }, + toggle_earlier_replies: function() { + var $earlier_replies = $(".child-post").slice(0, $(".child-post").length - 2); + var $btn = $(".btn-earlier-replies").on("click", function() { + if($earlier_replies.hasClass("hide")) { + $earlier_replies.removeClass("hide"); + $(".btn-earlier-label").html("Hide"); + } else { + $earlier_replies.addClass("hide"); + $(".btn-earlier-label").html("Show"); + } + }); + + if($earlier_replies.length) { + $btn.toggleClass("hide", false).click(); + } + }, + toggle_edit: function(only_owner) { + if(only_owner) { + var user = wn.get_cookie("user_id"); + $(".edit-post").each(function() { + $(this).toggleClass("hide", !(website.access.write && $(this).attr("data-owner")===user)); + }); + } else { + $(".edit-post").toggleClass("hide", !website.access.write); + } + }, + toggle_upvote: function() { + if(!website.access.read) { + $(".upvote").remove(); + } + }, + toggle_post_editor: function() { + $(".post-editor").toggleClass("hide", !website.access.write); + }, + setup_upvote: function() { + $(".post-list, .parent-post").on("click", ".upvote a", function() { + var sid = wn.get_cookie("sid"); + if(!sid || sid==="Guest") { + wn.msgprint("Please login to Upvote!"); + return; + } + var $post = $(this).parents(".post"); + var post = $post.attr("data-name"); + var $btn = $(this).prop("disabled", true); + + $.ajax({ + url: "/", + type: "POST", + data: { + cmd: "webnotes.website.doctype.user_vote.user_vote.set_vote", + ref_doctype: "Post", + ref_name: post + }, + statusCode: { + 200: function(data) { + if(data.exc) { + console.log(data.exc); + } else { + var text_class = data.message === "ok" ? "text-success" : "text-danger"; + if(data.message==="ok") { + var count = parseInt($post.find(".upvote-count").text()); + $post.find(".upvote-count").text(count + 1).removeClass("hide"); + } + $btn.addClass(text_class); + setTimeout(function() { $btn.removeClass(text_class); }, 2000); + } + } + } + }).always(function() { + $btn.prop("disabled", false); + }); + }); + }, + setup_autosuggest: function(opts) { + if(opts.$control.hasClass("ui-autocomplete-input")) return; + + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.min.js"); + wn.require("/assets/webnotes/js/lib/jquery/bootstrap_theme/jquery-ui.selected.css"); + + var $user_suggest = opts.$control.autocomplete({ + source: function(request, response) { + $.ajax({ + url: "/", + data: { + cmd: opts.method, + term: request.term, + group: website.group + }, + success: function(data) { + if(data.exc) { + console.log(data.exc); + } else { + response(data.message); + } + } + }); + }, + select: function(event, ui) { + opts.$control.val(""); + opts.select(ui.item.profile, ui.item); + } + }); + + $user_suggest.data( "ui-autocomplete" )._renderItem = function(ul, item) { + return $("
  • ").html("" + item.profile_html + "") + .css("padding", "5px") + .appendTo(ul); + }; + + return opts.$control + }, + setup_datepicker: function(opts) { + if(opts.$control.hasClass("hasDatetimepicker")) return; + + // libs required for datetime picker + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.min.js"); + wn.require("/assets/webnotes/js/lib/jquery/bootstrap_theme/jquery-ui.selected.css"); + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.slider.min.js"); + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.sliderAccess.js"); + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.timepicker-addon.css"); + wn.require("/assets/webnotes/js/lib/jquery/jquery.ui.timepicker-addon.js"); + + opts.$control.datetimepicker({ + timeFormat: "hh:mm tt", + dateFormat: 'dd-mm-yy', + changeYear: true, + yearRange: "-70Y:+10Y", + stepMinute: 5, + hour: 10, + onClose: opts.onClose + }); + + website.setup_datetime_functions(); + + return opts.$control; + }, + setup_datetime_functions: function() { + // requires datetime picker + wn.provide("website.datetimepicker"); + website.datetimepicker.str_to_obj = function(datetime_str) { + return $.datepicker.parseDateTime("yy-mm-dd", "HH:mm:ss", datetime_str); + }; + + website.datetimepicker.obj_to_str = function(datetime) { + if(!datetime) { + return ""; + } + // requires datepicker + var date_str = $.datepicker.formatDate("yy-mm-dd", datetime) + var time_str = $.datepicker.formatTime("HH:mm:ss", { + hour: datetime.getHours(), + minute: datetime.getMinutes(), + second: datetime.getSeconds() + }) + return date_str + " " + time_str; + }; + + website.datetimepicker.format_datetime = function(datetime) { + if (typeof(datetime)==="string") { + datetime = website.datetimepicker.str_to_obj(datetime); + } + var date_str = $.datepicker.formatDate("dd-mm-yy", datetime) + var time_str = $.datepicker.formatTime("hh:mm tt", { + hour: datetime.getHours(), + minute: datetime.getMinutes(), + second: datetime.getSeconds() + }) + return date_str + " " + time_str; + } + }, + setup_settings: function() { + // autosuggest + website.setup_autosuggest({ + $control: $(".add-user-control"), + select: function(value) { + website.add_sitemap_permission(value); + }, + method: "webnotes.templates.website_group.settings.suggest_user" + }); + + + // trigger for change permission + $(".permission-editor-area").on("click", ".sitemap-permission [type='checkbox']", + website.update_permission); + $(".permission-editor-area").find(".btn-add-group").on("click", website.add_group); + $(".btn-settings").parent().addClass("active"); + + // disabled public_write if not public_read + var control_public_read = $(".control-add-group-public_read").click(function() { + if(!$(this).prop("checked")) { + $(".control-add-group-public_write").prop("checked", false).prop("disabled", true); + } else { + $(".control-add-group-public_write").prop("disabled", false); + } + }).trigger("click").trigger("click"); // hack + }, + add_group: function() { + var $control = $(".control-add-group"), + $btn = $(".btn-add-group"); + + if($control.val()) { + $btn.prop("disabled", true); + $.ajax({ + url:"/", + type:"POST", + data: { + cmd:"webnotes.templates.website_group.settings.add_website_group", + group: website.group, + new_group: $control.val(), + group_type: $(".control-add-group-type").val(), + public_read: $(".control-add-group-public_read").is(":checked") ? 1 : 0, + public_write: $(".control-add-group-public_write").is(":checked") ? 1 : 0 + }, + statusCode: { + 403: function() { + wn.msgprint("Name Not Permitted"); + }, + 200: function(data) { + if(data.exc) { + console.log(data.exc); + if(data._server_messages) wn.msgprint(data._server_messages); + } else { + wn.msgprint("Group Added, refreshing..."); + setTimeout(function() { window.location.reload(); }, 1000) + } + } + } + }).always(function() { + $btn.prop("disabled",false); + $control.val(""); + }) + } + }, + update_permission: function() { + var $chk = $(this); + var $tr = $chk.parents("tr:first"); + $chk.prop("disabled", true); + + $.ajax({ + url: "/", + type: "POST", + data: { + cmd: "webnotes.templates.website_group.settings.update_permission", + profile: $tr.attr("data-profile"), + perm: $chk.attr("data-perm"), + value: $chk.prop("checked") ? "1" : "0", + sitemap_page: website.group + }, + statusCode: { + 403: function() { + wn.msgprint("Not Allowed"); + }, + 200: function(data) { + $chk.prop("disabled", false); + if(data.exc) { + $chk.prop("checked", !$chk.prop("checked")); + console.log(data.exc); + } else { + if(!$tr.find(":checked").length) $tr.remove(); + } + } + }, + }); + }, + add_sitemap_permission: function(profile) { + $.ajax({ + url: "/", + type: "POST", + data: { + cmd: "webnotes.templates.website_group.settings.add_sitemap_permission", + profile: profile, + sitemap_page: website.group + }, + success: function(data) { + $(".add-user-control").val(""); + if(data.exc) { + console.log(data.exc); + } else { + $(data.message).prependTo($(".permission-editor tbody")); + } + } + }); + }, + update_group_description: function() { + $(".btn-update-description").prop("disabled", true); + $.ajax({ + url: "/", + type: "POST", + data: { + cmd: "webnotes.templates.website_group.settings.update_description", + description: $(".control-description").val() || "", + group: website.group + }, + success: function(data) { + window.location.reload(); + } + }).always(function() { $(".btn-update-description").prop("disabled", false); }); + } +}); + +$(document).on("apply_permissions", function() { + website.toggle_permitted(); +}); diff --git a/webnotes/webutils.py b/webnotes/webutils.py index 4c8bd2270c..7a2d40741d 100644 --- a/webnotes/webutils.py +++ b/webnotes/webutils.py @@ -13,8 +13,6 @@ from urllib import quote import mimetypes from webnotes.website.doctype.website_sitemap.website_sitemap import add_to_sitemap, update_sitemap, remove_sitemap - -# for access as webnotes.webutils.fn from webnotes.website.doctype.website_sitemap_permission.website_sitemap_permission \ import get_access @@ -73,7 +71,6 @@ def build_json(page_name): def build_page(page_name): context = get_context(page_name) - context.update(get_website_settings()) html = webnotes.get_template(context.base_template_path).render(context) @@ -149,6 +146,7 @@ def build_context(sitemap_options): """get_context method of bean or module is supposed to render content templates and push it into context""" context = webnotes._dict({ "_": webnotes._ }) context.update(sitemap_options) + context.update(get_website_settings()) if sitemap_options.get("controller"): module = webnotes.get_module(sitemap_options.get("controller")) @@ -195,7 +193,7 @@ def get_website_settings(): "utils": webnotes.utils, "post_login": [ {"label": "Reset Password", "url": "update-password", "icon": "icon-key"}, - {"label": "Logout", "url": "/?cmd=web_logout", "icon": "icon-signout"} + {"label": "Logout", "url": "?cmd=web_logout", "icon": "icon-signout"} ] }) @@ -222,6 +220,10 @@ def get_website_settings(): context.web_include_js = hooks.web_include_js or [] context.web_include_css = hooks.web_include_css or [] + # get settings from site config + if webnotes.conf.get("fb_app_id"): + context.fb_app_id = webnotes.conf.fb_app_id + return context def is_ajax(): @@ -342,7 +344,9 @@ class WebsiteGenerator(DocListController): "page_name": page_name, "link_name": self._website_config.name, "lastmod": webnotes.utils.get_datetime(self.doc.modified).strftime("%Y-%m-%d"), - "parent_website_sitemap": self.doc.parent_website_sitemap + "parent_website_sitemap": self.doc.parent_website_sitemap, + "page_title": self.get_page_title() \ + if hasattr(self, "get_page_title") else (self.doc.title or self.doc.name) }) if self.meta.get_field("public_read"):