From ba69aabee1f78a2a33cf4c1b2e0b838a2979772a Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Fri, 1 Sep 2017 23:08:09 -0700 Subject: [PATCH 01/12] Initial minimum for Gatherings --- coursebuilder/modules/gatherings/__init__.py | 0 .../modules/gatherings/gatherings.py | 663 ++++++++++++++++++ .../modules/gatherings/manifest.yaml | 14 + coursebuilder/modules/gatherings/messages.py | 15 + .../gatherings/templates/gathering_list.html | 70 ++ coursebuilder/views/gatherings.html | 69 ++ 6 files changed, 831 insertions(+) create mode 100644 coursebuilder/modules/gatherings/__init__.py create mode 100644 coursebuilder/modules/gatherings/gatherings.py create mode 100644 coursebuilder/modules/gatherings/manifest.yaml create mode 100644 coursebuilder/modules/gatherings/messages.py create mode 100644 coursebuilder/modules/gatherings/templates/gathering_list.html create mode 100644 coursebuilder/views/gatherings.html diff --git a/coursebuilder/modules/gatherings/__init__.py b/coursebuilder/modules/gatherings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py new file mode 100644 index 00000000..e83f7ddf --- /dev/null +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -0,0 +1,663 @@ +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes and methods to create and manage Gatherings.""" + +__author__ = 'Saifu Angto (saifu@google.com)' + + +import cgi +import collections +import datetime +import os +import urllib + +import jinja2 + +import appengine_config +from common import crypto +from common import tags +from common import utils as common_utils +from common import schema_fields +from common import resource +from common import utc +from controllers import sites +from controllers import utils +from models import resources_display +from models import courses +from models import custom_modules +from models import entities +from models import models +from models import roles +from models import transforms +from modules.gatherings import messages +from modules.dashboard import dashboard +from modules.i18n_dashboard import i18n_dashboard +from modules.news import news +from modules.oeditor import oeditor + +from google.appengine.ext import db + +MODULE_NAME = 'gatherings' +MODULE_TITLE = 'Gatherings' +TEMPLATE_DIR = os.path.join( + appengine_config.BUNDLE_ROOT, 'modules', MODULE_NAME, 'templates') + + +class GatheringsRights(object): + """Manages view/edit rights for gatherings.""" + + @classmethod + def can_view(cls, unused_handler): + return True + + @classmethod + def can_edit(cls, handler): + return roles.Roles.is_course_admin(handler.app_context) + + @classmethod + def can_delete(cls, handler): + return cls.can_edit(handler) + + @classmethod + def can_add(cls, handler): + return cls.can_edit(handler) + + @classmethod + def apply_rights(cls, handler, items): + """Filter out items that current user can't see.""" + if GatheringsRights.can_edit(handler): + return items + + allowed = [] + for item in items: + if not item.is_draft: + allowed.append(item) + + return allowed + + +class GatheringsHandlerMixin(object): + def get_gathering_action_url(self, action, key=None): + args = {'action': action} + if key: + args['key'] = key + return self.canonicalize_url( + '{}?{}'.format( + GatheringsDashboardHandler.URL, urllib.urlencode(args))) + + def format_items_for_template(self, items): + """Formats a list of entities into template values.""" + template_items = [] + for item in items: + item = transforms.entity_to_dict(item) + date = item.get('date') + if date: + date = datetime.datetime.combine( + date, datetime.time(0, 0, 0, 0)) + item['date'] = ( + date - datetime.datetime(1970, 1, 1)).total_seconds() * 1000 + + # add 'edit' actions + if GatheringsRights.can_edit(self): + item['edit_action'] = self.get_gathering_action_url( + GatheringsDashboardHandler.EDIT_ACTION, key=item['key']) + + item['delete_xsrf_token'] = self.create_xsrf_token( + GatheringsDashboardHandler.DELETE_ACTION) + item['delete_action'] = self.get_gathering_action_url( + GatheringsDashboardHandler.DELETE_ACTION, + key=item['key']) + + template_items.append(item) + + output = {} + output['children'] = template_items + + # add 'add' action + if GatheringsRights.can_edit(self): + output['add_xsrf_token'] = self.create_xsrf_token( + GatheringsDashboardHandler.ADD_ACTION) + output['add_action'] = self.get_gathering_action_url( + GatheringsDashboardHandler.ADD_ACTION) + + return output + + +class GatheringsStudentHandler( + GatheringsHandlerMixin, utils.BaseHandler, + utils.ReflectiveRequestHandler): + URL = '/gatherings' + default_action = 'list' + get_actions = [default_action] + post_actions = [] + + def get_list(self): + """Shows a list of gatherings.""" + student = None + user = self.personalize_page_and_get_user() + transient_student = False + if user is None: + transient_student = True + else: + student = models.Student.get_enrolled_student_by_user(user) + if not student: + transient_student = True + self.template_value['transient_student'] = transient_student + locale = self.app_context.get_current_locale() + if locale == self.app_context.default_locale: + locale = None + items = GatheringEntity.get_gatherings(locale=locale) + items = GatheringsRights.apply_rights(self, items) + self.template_value['gatherings'] = self.format_items_for_template( + items) + self._render() + + def _render(self): + self.template_value['navbar'] = {'gatherings': True} + self.render('gatherings.html') + + +class GatheringsDashboardHandler( + GatheringsHandlerMixin, dashboard.DashboardHandler): + """Handler for gatherings.""" + + LIST_ACTION = 'edit_gatherings' + EDIT_ACTION = 'edit_gathering' + DELETE_ACTION = 'delete_gathering' + ADD_ACTION = 'add_gathering' + DEFAULT_TITLE_TEXT = 'New Gathering' + + get_actions = [LIST_ACTION, EDIT_ACTION] + post_actions = [ADD_ACTION, DELETE_ACTION] + + LINK_URL = 'edit_gatherings' + URL = '/{}'.format(LINK_URL) + LIST_URL = '{}?action={}'.format(LINK_URL, LIST_ACTION) + + @classmethod + def get_child_routes(cls): + """Add child handlers for REST.""" + return [ + (GatheringsItemRESTHandler.URL, GatheringsItemRESTHandler)] + + def get_edit_gatherings(self): + """Shows a list of gatherings.""" + items = GatheringEntity.get_gatherings() + items = GatheringsRights.apply_rights(self, items) + + main_content = self.get_template( + 'gathering_list.html', [TEMPLATE_DIR]).render({ + 'gatherings': self.format_items_for_template(items), + 'status_xsrf_token': self.create_xsrf_token( + GatheringsItemRESTHandler.STATUS_ACTION) + }) + + self.render_page({ + 'page_title': self.format_title('Gatherings'), + 'main_content': jinja2.utils.Markup(main_content)}) + + def get_edit_gathering(self): + """Shows an editor for an gathering.""" + + key = self.request.get('key') + + schema = GatheringsItemRESTHandler.SCHEMA() + + exit_url = self.canonicalize_url('/{}'.format(self.LIST_URL)) + rest_url = self.canonicalize_url('/rest/gatherings/item') + form_html = oeditor.ObjectEditor.get_html_for( + self, + schema.get_json_schema(), + schema.get_schema_dict(), + key, rest_url, exit_url, + delete_method='delete', + delete_message='Are you sure you want to delete this gathering?', + delete_url=self._get_delete_url( + GatheringsItemRESTHandler.URL, key, 'gathering-delete'), + display_types=schema.get_display_types()) + + self.render_page({ + 'main_content': form_html, + 'page_title': 'Edit Gatherings', + }, in_action=self.LIST_ACTION) + + def _get_delete_url(self, base_url, key, xsrf_token_name): + return '%s?%s' % ( + self.canonicalize_url(base_url), + urllib.urlencode({ + 'key': key, + 'xsrf_token': cgi.escape( + self.create_xsrf_token(xsrf_token_name)), + })) + + def post_delete_gathering(self): + """Deletes an gathering.""" + if not GatheringsRights.can_delete(self): + self.error(401) + return + + key = self.request.get('key') + entity = GatheringEntity.get(key) + if entity: + entity.delete() + self.redirect('/{}'.format(self.LIST_URL)) + + def post_add_gathering(self): + """Adds a new gathering and redirects to an editor for it.""" + if not GatheringsRights.can_add(self): + self.error(401) + return + + entity = GatheringEntity.make(self.DEFAULT_TITLE_TEXT, '', True) + entity.put() + + self.redirect(self.get_gathering_action_url( + self.EDIT_ACTION, key=entity.key())) + + +class GatheringsItemRESTHandler(utils.BaseRESTHandler): + """Provides REST API for an gathering.""" + + URL = '/rest/gatherings/item' + + ACTION = 'gathering-put' + STATUS_ACTION = 'set_draft_status_gathering' + + @classmethod + def SCHEMA(cls): + schema = schema_fields.FieldRegistry('Gathering', + extra_schema_dict_values={ + 'className': 'inputEx-Group new-form-layout'}) + schema.add_property(schema_fields.SchemaField( + 'key', 'ID', 'string', editable=False, hidden=True)) + schema.add_property(schema_fields.SchemaField( + 'title', 'Title', 'string', + description=messages.GATHERING_TITLE_DESCRIPTION)) + schema.add_property(schema_fields.SchemaField( + 'html', 'Body', 'html', + description=messages.GATHERING_BODY_DESCRIPTION, + extra_schema_dict_values={ + 'supportCustomTags': tags.CAN_USE_DYNAMIC_TAGS.value, + 'excludedCustomTags': tags.EditorBlacklists.COURSE_SCOPE}, + optional=True)) + schema.add_property(schema_fields.SchemaField( + 'date', 'Date', 'datetime', + description=messages.GATHERING_DATE_DESCRIPTION, + extra_schema_dict_values={ + '_type': 'datetime', + 'className': 'inputEx-CombineField gcb-datetime ' + 'inputEx-fieldWrapper date-only inputEx-required'})) + schema.add_property(schema_fields.SchemaField( + 'is_draft', 'Status', 'boolean', + description=messages.GATHERING_STATUS_DESCRIPTION, + extra_schema_dict_values={'className': 'split-from-main-group'}, + optional=True, + select_data=[ + (True, resources_display.DRAFT_TEXT), + (False, resources_display.PUBLISHED_TEXT)])) + return schema + + def get(self): + """Handles REST GET verb and returns an object as JSON payload.""" + key = self.request.get('key') + + try: + entity = GatheringEntity.get(key) + except db.BadKeyError: + entity = None + + if not entity: + transforms.send_json_response( + self, 404, 'Object not found.', {'key': key}) + return + viewable = GatheringsRights.apply_rights(self, [entity]) + if not viewable: + transforms.send_json_response( + self, 401, 'Access denied.', {'key': key}) + return + entity = viewable[0] + + schema = GatheringsItemRESTHandler.SCHEMA() + + entity_dict = transforms.entity_to_dict(entity) + + # Format the internal date object as ISO 8601 datetime, with time + # defaulting to 00:00:00 + date = entity_dict['date'] + date = datetime.datetime(date.year, date.month, date.day) + entity_dict['date'] = date + + json_payload = transforms.dict_to_json(entity_dict) + transforms.send_json_response( + self, 200, 'Success.', + payload_dict=json_payload, + xsrf_token=crypto.XsrfTokenManager.create_xsrf_token(self.ACTION)) + + def put(self): + """Handles REST PUT verb with JSON payload.""" + request = transforms.loads(self.request.get('request')) + key = request.get('key') + + if not self.assert_xsrf_token_or_fail( + request, self.ACTION, {'key': key}): + return + + if not GatheringsRights.can_edit(self): + transforms.send_json_response( + self, 401, 'Access denied.', {'key': key}) + return + + entity = GatheringEntity.get(key) + if not entity: + transforms.send_json_response( + self, 404, 'Object not found.', {'key': key}) + return + + schema = GatheringsItemRESTHandler.SCHEMA() + + payload = request.get('payload') + update_dict = transforms.json_to_dict( + transforms.loads(payload), schema.get_json_schema_dict()) + if entity.is_draft and not update_dict.get('set_draft'): + item = news.NewsItem( + str(TranslatableResourceGathering.key_for_entity(entity)), + GatheringsStudentHandler.URL.lstrip('/')) + news.CourseNewsDao.add_news_item(item) + + # The datetime widget returns a datetime object and we need a UTC date. + update_dict['date'] = update_dict['date'].date() + del update_dict['key'] # Don't overwrite key member method in entity. + transforms.dict_to_entity(entity, update_dict) + + entity.put() + + transforms.send_json_response(self, 200, 'Saved.') + + def delete(self): + """Deletes an gathering.""" + key = self.request.get('key') + + if not self.assert_xsrf_token_or_fail( + self.request, 'gathering-delete', {'key': key}): + return + + if not GatheringsRights.can_delete(self): + self.error(401) + return + + entity = GatheringEntity.get(key) + if not entity: + transforms.send_json_response( + self, 404, 'Object not found.', {'key': key}) + return + + entity.delete() + + transforms.send_json_response(self, 200, 'Deleted.') + + @classmethod + def post_set_draft_status(cls, handler): + """Sets the draft status of a course component. + + Only works with CourseModel13 courses, but the REST handler + is only called with this type of courses. + + XSRF is checked in the dashboard. + """ + key = handler.request.get('key') + + if not GatheringsRights.can_edit(handler): + transforms.send_json_response( + handler, 401, 'Access denied.', {'key': key}) + return + + entity = GatheringEntity.get(key) + if not entity: + transforms.send_json_response( + handler, 404, 'Object not found.', {'key': key}) + return + + set_draft = handler.request.get('set_draft') + if set_draft == '1': + set_draft = True + elif set_draft == '0': + set_draft = False + else: + transforms.send_json_response( + handler, 401, 'Invalid set_draft value, expected 0 or 1.', + {'set_draft': set_draft} + ) + return + + if entity.is_draft and not set_draft: + item = news.NewsItem( + str(TranslatableResourceGathering.key_for_entity(entity)), + GatheringsStudentHandler.URL.lstrip('/')) + news.CourseNewsDao.add_news_item(item) + + entity.is_draft = set_draft + entity.put() + + transforms.send_json_response( + handler, + 200, + 'Draft status set to %s.' % ( + resources_display.DRAFT_TEXT if set_draft else + resources_display.PUBLISHED_TEXT + ), { + 'is_draft': set_draft + } + ) + return + + +class GatheringEntity(entities.BaseEntity): + """A class that represents a persistent database entity of gatherings. + + Note that this class was added to Course Builder prior to the idioms + introduced in models.models.BaseJsonDao and friends. That being the + case, this class is much more hand-coded and not well integrated into + the structure of callbacks and hooks that have accumulated around + entity caching, i18n, and the like. + """ + + title = db.StringProperty(indexed=False) + date = db.DateProperty() + html = db.TextProperty(indexed=False) + is_draft = db.BooleanProperty() + + _MEMCACHE_KEY = 'gatherings' + + @classmethod + def get_gatherings(cls, locale=None): + memcache_key = cls._cache_key(locale) + items = models.MemcacheManager.get(memcache_key) + if items is None: + items = list(common_utils.iter_all(GatheringEntity.all())) + items.sort(key=lambda item: item.date, reverse=True) + if locale: + cls._translate_content(items) + + # TODO(psimakov): prepare to exceed 1MB max item size + # read more here: http://stackoverflow.com + # /questions/5081502/memcache-1-mb-limit-in-google-app-engine + models.MemcacheManager.set(memcache_key, items) + return items + + @classmethod + def _cache_key(cls, locale=None): + if not locale: + return cls._MEMCACHE_KEY + return cls._MEMCACHE_KEY + ':' + locale + + @classmethod + def purge_cache(cls, locale=None): + models.MemcacheManager.delete(cls._cache_key(locale)) + + @classmethod + def make(cls, title, html, is_draft): + entity = cls() + entity.title = title + entity.date = utc.now_as_datetime().date() + entity.html = html + entity.is_draft = is_draft + return entity + + def put(self): + """Do the normal put() and also invalidate memcache.""" + result = super(GatheringEntity, self).put() + self.purge_cache() + if i18n_dashboard.I18nProgressDeferredUpdater.is_translatable_course(): + i18n_dashboard.I18nProgressDeferredUpdater.update_resource_list( + [TranslatableResourceGathering.key_for_entity(self)]) + return result + + def delete(self): + """Do the normal delete() and invalidate memcache.""" + news.CourseNewsDao.remove_news_item( + str(TranslatableResourceGathering.key_for_entity(self))) + super(GatheringEntity, self).delete() + self.purge_cache() + + @classmethod + def _translate_content(cls, items): + app_context = sites.get_course_for_current_request() + course = courses.Course.get(app_context) + key_list = [ + TranslatableResourceGathering.key_for_entity(item) + for item in items] + FakeDto = collections.namedtuple('FakeDto', ['dict']) + fake_items = [ + FakeDto({'title': item.title, 'html': item.html}) + for item in items] + i18n_dashboard.translate_dto_list(course, fake_items, key_list) + for item, fake_item in zip(items, fake_items): + item.title = str(fake_item.dict['title']) + item.html = str(fake_item.dict['html']) + + +class TranslatableResourceGathering( + i18n_dashboard.AbstractTranslatableResourceType): + + @classmethod + def get_ordering(cls): + return i18n_dashboard.TranslatableResourceRegistry.ORDERING_LAST + + @classmethod + def get_title(cls): + return MODULE_TITLE + + @classmethod + def key_for_entity(cls, gathering, course=None): + return resource.Key(ResourceHandlerGathering.TYPE, + gathering.key().id(), course) + + @classmethod + def get_resources_and_keys(cls, course): + return [(gathering, cls.key_for_entity(gathering, course)) + for gathering in GatheringEntity.get_gatherings()] + + @classmethod + def get_resource_types(cls): + return [ResourceHandlerGathering.TYPE] + + @classmethod + def notify_translations_changed(cls, resource_bundle_key): + GatheringEntity.purge_cache(resource_bundle_key.locale) + + @classmethod + def get_i18n_title(cls, resource_key): + locale = None + app_context = sites.get_course_for_current_request() + if (app_context and + app_context.default_locale != app_context.get_current_locale()): + locale = app_context.get_current_locale() + gatherings = GatheringEntity.get_gatherings(locale) + item = common_utils.find( + lambda a: a.key().id() == int(resource_key.key), gatherings) + return item.title if item else None + + +class ResourceHandlerGathering(resource.AbstractResourceHandler): + """Generic resoruce accessor for applying translations to gatherings.""" + + TYPE = 'gathering' + + @classmethod + def _entity_key(cls, key): + return db.Key.from_path(GatheringEntity.kind(), int(key)) + + @classmethod + def get_resource(cls, course, key): + return GatheringEntity.get(cls._entity_key(key)) + + @classmethod + def get_resource_title(cls, rsrc): + return rsrc.title + + @classmethod + def get_schema(cls, course, key): + return GatheringsItemRESTHandler.SCHEMA() + + @classmethod + def get_data_dict(cls, course, key): + entity = cls.get_resource(course, key) + return transforms.entity_to_dict(entity) + + @classmethod + def get_view_url(cls, rsrc): + return GatheringsStudentHandler.URL.lstrip('/') + + @classmethod + def get_edit_url(cls, key): + return (GatheringsDashboardHandler.LINK_URL + '?' + + urllib.urlencode({ + 'action': GatheringsDashboardHandler.EDIT_ACTION, + 'key': cls._entity_key(key), + })) + + +custom_module = None + + +def on_module_enabled(): + resource.Registry.register(ResourceHandlerGathering) + i18n_dashboard.TranslatableResourceRegistry.register( + TranslatableResourceGathering) + + +def register_module(): + """Registers this module in the registry.""" + + handlers = [ + (handler.URL, handler) for handler in + [GatheringsStudentHandler, GatheringsDashboardHandler]] + + dashboard.DashboardHandler.add_sub_nav_mapping( + 'analytics', MODULE_NAME, MODULE_TITLE, + action=GatheringsDashboardHandler.LIST_ACTION, + href=GatheringsDashboardHandler.LIST_URL, + placement=1000, sub_group_name='pinned') + + dashboard.DashboardHandler.add_custom_post_action( + GatheringsItemRESTHandler.STATUS_ACTION, + GatheringsItemRESTHandler.post_set_draft_status) + + global custom_module # pylint: disable=global-statement + custom_module = custom_modules.Module( + MODULE_TITLE, + 'A set of pages for managing course gatherings.', + [], handlers, notify_module_enabled=on_module_enabled) + return custom_module diff --git a/coursebuilder/modules/gatherings/manifest.yaml b/coursebuilder/modules/gatherings/manifest.yaml new file mode 100644 index 00000000..1aa9fa4d --- /dev/null +++ b/coursebuilder/modules/gatherings/manifest.yaml @@ -0,0 +1,14 @@ +registration: + main_module: modules.gatherings.gatherings + +# tests: +# functional: +# - modules.gatherings.gatherings_tests.GatheringsTests = 21 + +files: + - modules/gatherings/__init__.py + - modules/gatherings/gatherings.py +# - modules/gatherings/gatherings_tests.py +# - modules/gatherings/templates/gathering_list.html + - modules/gatherings/manifest.yaml + - modules/gatherings/messages.py diff --git a/coursebuilder/modules/gatherings/messages.py b/coursebuilder/modules/gatherings/messages.py new file mode 100644 index 00000000..7623d66f --- /dev/null +++ b/coursebuilder/modules/gatherings/messages.py @@ -0,0 +1,15 @@ +GATHERING_TITLE_DESCRIPTION = """ +This is the title of the gathering. +""" + +GATHERING_BODY_DESCRIPTION = """ +This is the message of the gathering. +""" + +GATHERING_DATE_DESCRIPTION = """ +This places a date on your gathering. +""" + +GATHERING_STATUS_DESCRIPTION = """ +Your students will not see this gathering until you select "Public". +""" diff --git a/coursebuilder/modules/gatherings/templates/gathering_list.html b/coursebuilder/modules/gatherings/templates/gathering_list.html new file mode 100644 index 00000000..3f43fc90 --- /dev/null +++ b/coursebuilder/modules/gatherings/templates/gathering_list.html @@ -0,0 +1,70 @@ +{% if gatherings.add_action %} +
+
+ + +
+
+{% endif %} + +
+ + + + + + + + + + + {% for item in gatherings.children %} + + + + + + + + + + + + {% endfor %} + +
Title
+
+
+ open_in_new + + + {{item.title}} + + + {% if item.delete_action %} +
+ + +
+ {% endif %} +
+ {% if not gatherings.children|length %} +
+
No items
+
+ {% endif %} +
diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html new file mode 100644 index 00000000..da456294 --- /dev/null +++ b/coursebuilder/views/gatherings.html @@ -0,0 +1,69 @@ +{% extends 'base_course.html' %} + +{% block subtitle %} + {# I18N: Title of the webpage. #} + - {{ gettext('Gatherings') }} +{% endblock subtitle %} + +{% block assets %} + {{ super() }} + +{% endblock %} + +{% block top_content %} +{% endblock %} + +{% block main_content %} +
+
+
+ {% if gatherings %} + {% if gatherings.add_action %} +
+ + +
+
+ {% endif %} + {% if not gatherings.children %} + {# I18N: Shown if the list of gatherings is empty. #} + {# TODO (chaks): Change the class #} +

+ {{ gettext('Currently, there are no gatherings.') }} +

+ {% endif %} + {% for item in gatherings.children %} +
+

+ + {# TODO (chaks): Change the class #} + + {{ item.title }} + {% if item.is_draft %}(Private){% endif %} + + {% if item.edit_action %} + edit + {% endif %} +

+

Start Time: {{item.start_time}}

+

End Time: {{item.end_time}}

+ + {# TODO (chaks): Change the class #} +

+ {{ item.html | gcb_tags }} +

+ {% endfor %} + {% else %} + {{ content }} + {% endif %} +
+
+
+{% endblock %} From 20a4395a6ce3dd593eb53859cd18d8af708178b8 Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sat, 2 Sep 2017 14:39:29 -0700 Subject: [PATCH 02/12] Added start_time and end_time to Gatherings --- .../modules/gatherings/gatherings.py | 34 ++++++++----------- coursebuilder/views/base_course.html | 4 +++ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index e83f7ddf..0bf38b00 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -102,13 +102,6 @@ def format_items_for_template(self, items): template_items = [] for item in items: item = transforms.entity_to_dict(item) - date = item.get('date') - if date: - date = datetime.datetime.combine( - date, datetime.time(0, 0, 0, 0)) - item['date'] = ( - date - datetime.datetime(1970, 1, 1)).total_seconds() * 1000 - # add 'edit' actions if GatheringsRights.can_edit(self): item['edit_action'] = self.get_gathering_action_url( @@ -293,12 +286,19 @@ def SCHEMA(cls): 'excludedCustomTags': tags.EditorBlacklists.COURSE_SCOPE}, optional=True)) schema.add_property(schema_fields.SchemaField( - 'date', 'Date', 'datetime', + 'start_time', 'Start Time', 'datetime', description=messages.GATHERING_DATE_DESCRIPTION, extra_schema_dict_values={ '_type': 'datetime', 'className': 'inputEx-CombineField gcb-datetime ' - 'inputEx-fieldWrapper date-only inputEx-required'})) + 'inputEx-fieldWrapper inputEx-required'})) + schema.add_property(schema_fields.SchemaField( + 'end_time', 'End Time', 'datetime', + description=messages.GATHERING_DATE_DESCRIPTION, + extra_schema_dict_values={ + '_type': 'datetime', + 'className': 'inputEx-CombineField gcb-datetime ' + 'inputEx-fieldWrapper inputEx-required'})) schema.add_property(schema_fields.SchemaField( 'is_draft', 'Status', 'boolean', description=messages.GATHERING_STATUS_DESCRIPTION, @@ -333,12 +333,6 @@ def get(self): entity_dict = transforms.entity_to_dict(entity) - # Format the internal date object as ISO 8601 datetime, with time - # defaulting to 00:00:00 - date = entity_dict['date'] - date = datetime.datetime(date.year, date.month, date.day) - entity_dict['date'] = date - json_payload = transforms.dict_to_json(entity_dict) transforms.send_json_response( self, 200, 'Success.', @@ -376,8 +370,6 @@ def put(self): GatheringsStudentHandler.URL.lstrip('/')) news.CourseNewsDao.add_news_item(item) - # The datetime widget returns a datetime object and we need a UTC date. - update_dict['date'] = update_dict['date'].date() del update_dict['key'] # Don't overwrite key member method in entity. transforms.dict_to_entity(entity, update_dict) @@ -474,7 +466,8 @@ class GatheringEntity(entities.BaseEntity): """ title = db.StringProperty(indexed=False) - date = db.DateProperty() + start_time = db.DateTimeProperty() + end_time = db.DateTimeProperty() html = db.TextProperty(indexed=False) is_draft = db.BooleanProperty() @@ -486,7 +479,7 @@ def get_gatherings(cls, locale=None): items = models.MemcacheManager.get(memcache_key) if items is None: items = list(common_utils.iter_all(GatheringEntity.all())) - items.sort(key=lambda item: item.date, reverse=True) + items.sort(key=lambda item: item.start_time, reverse=True) if locale: cls._translate_content(items) @@ -510,7 +503,8 @@ def purge_cache(cls, locale=None): def make(cls, title, html, is_draft): entity = cls() entity.title = title - entity.date = utc.now_as_datetime().date() + entity.start_time = utc.now_as_datetime() + entity.end_time = entitity.start_time + datetime.timedelta(minutes=30) entity.html = html entity.is_draft = is_draft return entity diff --git a/coursebuilder/views/base_course.html b/coursebuilder/views/base_course.html index 3f5403cc..90f04d73 100644 --- a/coursebuilder/views/base_course.html +++ b/coursebuilder/views/base_course.html @@ -37,6 +37,10 @@ {# I18N: Navbar tab. #} {{ gettext('Announcements') }} +
  • + {# I18N: Navbar tab. #} + {{ gettext('Gatherings') }} +
  • {# I18N: Navbar tab. #} {{ gettext('Course') }} From 398917ea5e7fbc84320a6524452ebec5382bec9a Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sat, 2 Sep 2017 23:46:25 -0700 Subject: [PATCH 03/12] Added GatheringsUsersEntity Relationship --- .../modules/gatherings/gatherings.py | 29 ++++++++++++++++--- coursebuilder/views/gatherings.html | 17 ++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index 0bf38b00..c67f7a0f 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -40,6 +40,7 @@ from models import entities from models import models from models import roles +from models.student_work import KeyProperty from models import transforms from modules.gatherings import messages from modules.dashboard import dashboard @@ -97,12 +98,26 @@ def get_gathering_action_url(self, action, key=None): '{}?{}'.format( GatheringsDashboardHandler.URL, urllib.urlencode(args))) - def format_items_for_template(self, items): + def format_items_for_template(self, items, user=None): """Formats a list of entities into template values.""" template_items = [] - for item in items: - item = transforms.entity_to_dict(item) + + joined_gatherings = {} + if user: + joined_gatherings = { + gu.gathering + for gu in + GatheringsUsersEntity + .all() + .filter('user =', user.user_id()) + .filter('gathering IN', [i.key() for i in items]) + } + for item_entitiy in items: + item = transforms.entity_to_dict(item_entitiy) + if user: + item['joined'] = item_entitiy.key() in joined_gatherings # add 'edit' actions + if GatheringsRights.can_edit(self): item['edit_action'] = self.get_gathering_action_url( GatheringsDashboardHandler.EDIT_ACTION, key=item['key']) @@ -154,7 +169,9 @@ def get_list(self): items = GatheringEntity.get_gatherings(locale=locale) items = GatheringsRights.apply_rights(self, items) self.template_value['gatherings'] = self.format_items_for_template( - items) + items, + self.get_user(), + ) self._render() def _render(self): @@ -542,6 +559,10 @@ def _translate_content(cls, items): item.html = str(fake_item.dict['html']) +class GatheringsUsersEntity(entities.BaseEntity): + gathering = KeyProperty(kind=GatheringEntity.kind()) + user = db.StringProperty() + class TranslatableResourceGathering( i18n_dashboard.AbstractTranslatableResourceType): diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index da456294..888fa2ee 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -15,6 +15,14 @@ {% endblock %} {% block main_content %} +
    @@ -54,7 +62,14 @@

    Start Time: {{item.start_time}}

    End Time: {{item.end_time}}

    - +

    + +

    {# TODO (chaks): Change the class #}

    {{ item.html | gcb_tags }} From 4563f6b5e8da296b16c617beb02c3f3305c83d65 Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 03:13:56 -0700 Subject: [PATCH 04/12] Join and Leave operation --- .../modules/gatherings/gatherings.py | 47 ++++++++++++++++- coursebuilder/views/gatherings.html | 50 +++++++++++++++---- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index c67f7a0f..2fc88c69 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -279,7 +279,7 @@ def post_add_gathering(self): class GatheringsItemRESTHandler(utils.BaseRESTHandler): """Provides REST API for an gathering.""" - + #TODO (chaks): Dont like the /item here URL = '/rest/gatherings/item' ACTION = 'gathering-put' @@ -356,6 +356,49 @@ def get(self): payload_dict=json_payload, xsrf_token=crypto.XsrfTokenManager.create_xsrf_token(self.ACTION)) + def post(self): + '''Handles adding of participants''' + # TODO (chaks): Handle Unjoining of event. + key = self.request.get('key') + join = self.request.get('action') == 'join' + gathering = GatheringEntity.get(key) if key else None + if not gathering: + transforms.send_json_response( + self, + 404, + 'Object not found', + {'key', key} + ) + return + + user = self.get_user() + # TODO (chaks) Check for if user is a student + # Check if already a participants + existing = GatheringsUsersEntity.all().filter( + 'user =', + user.user_id() + ).filter( + 'gathering = ', + gathering.key(), + ).get() + if existing and join: + transforms.send_json_response(self, 200, {'msg':'Already Participant.'}) + return + if not existing and not join: + transforms.send_json_response(self, 200, {'msg':'Already Left.'}) + return + + if join: + GatheringsUsersEntity( + gathering=gathering.key(), + user=user.user_id(), + ).put() + transforms.send_json_response(self, 200, {'msg': 'Added Participant'}) + return + existing.delete() + transforms.send_json_response(self, 200, {'msg': 'Removed Participant'}) + + def put(self): """Handles REST PUT verb with JSON payload.""" request = transforms.loads(self.request.get('request')) @@ -521,7 +564,7 @@ def make(cls, title, html, is_draft): entity = cls() entity.title = title entity.start_time = utc.now_as_datetime() - entity.end_time = entitity.start_time + datetime.timedelta(minutes=30) + entity.end_time = entity.start_time + datetime.timedelta(minutes=30) entity.html = html entity.is_draft = is_draft return entity diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index 888fa2ee..897e5c29 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -17,10 +17,31 @@ {% block main_content %}

    @@ -62,17 +83,26 @@

    Start Time: {{item.start_time}}

    End Time: {{item.end_time}}

    + + {# TODO (chaks): Change the class #}

    + {{ item.html | gcb_tags }} +

    +

    + -

    - {# TODO (chaks): Change the class #} -

    - {{ item.html | gcb_tags }}

    {% endfor %} {% else %} From 1b8dfc4ee43ffbbeca676f42f32da38c92f3d4c6 Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 04:40:51 -0700 Subject: [PATCH 05/12] Added Link to Video Conference --- .../modules/gatherings/gatherings.py | 22 +++++++++++++++---- coursebuilder/modules/gatherings/messages.py | 8 +++++-- coursebuilder/views/gatherings.html | 19 +++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index 2fc88c69..333cee27 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -22,6 +22,7 @@ import datetime import os import urllib +import random import jinja2 @@ -116,8 +117,17 @@ def format_items_for_template(self, items, user=None): item = transforms.entity_to_dict(item_entitiy) if user: item['joined'] = item_entitiy.key() in joined_gatherings - # add 'edit' actions + now = utc.now_as_datetime() + if now < item_entitiy.start_time: + item['status'] = 'future' + del item['vc_url'] + elif now > item_entitiy.end_time: + item['status'] = 'past' + del item['vc_url'] + else: + item['status'] = 'present' + # add 'edit' actions if GatheringsRights.can_edit(self): item['edit_action'] = self.get_gathering_action_url( GatheringsDashboardHandler.EDIT_ACTION, key=item['key']) @@ -302,16 +312,19 @@ def SCHEMA(cls): 'supportCustomTags': tags.CAN_USE_DYNAMIC_TAGS.value, 'excludedCustomTags': tags.EditorBlacklists.COURSE_SCOPE}, optional=True)) + schema.add_property(schema_fields.SchemaField( + 'vc_url', 'VC URL', 'url', editable=False, + description='Link to Video Conference')) schema.add_property(schema_fields.SchemaField( 'start_time', 'Start Time', 'datetime', - description=messages.GATHERING_DATE_DESCRIPTION, + description=messages.GATHERING_START_TIME_DESCRIPTION, extra_schema_dict_values={ '_type': 'datetime', 'className': 'inputEx-CombineField gcb-datetime ' 'inputEx-fieldWrapper inputEx-required'})) schema.add_property(schema_fields.SchemaField( 'end_time', 'End Time', 'datetime', - description=messages.GATHERING_DATE_DESCRIPTION, + description=messages.GATHERING_END_TIME_DESCRIPTION, extra_schema_dict_values={ '_type': 'datetime', 'className': 'inputEx-CombineField gcb-datetime ' @@ -358,7 +371,6 @@ def get(self): def post(self): '''Handles adding of participants''' - # TODO (chaks): Handle Unjoining of event. key = self.request.get('key') join = self.request.get('action') == 'join' gathering = GatheringEntity.get(key) if key else None @@ -530,6 +542,7 @@ class GatheringEntity(entities.BaseEntity): end_time = db.DateTimeProperty() html = db.TextProperty(indexed=False) is_draft = db.BooleanProperty() + vc_url = db.StringProperty() _MEMCACHE_KEY = 'gatherings' @@ -567,6 +580,7 @@ def make(cls, title, html, is_draft): entity.end_time = entity.start_time + datetime.timedelta(minutes=30) entity.html = html entity.is_draft = is_draft + entity.vc_url = 'https://meet.jit.si/%032x' % random.getrandbits(128) return entity def put(self): diff --git a/coursebuilder/modules/gatherings/messages.py b/coursebuilder/modules/gatherings/messages.py index 7623d66f..4ffcb100 100644 --- a/coursebuilder/modules/gatherings/messages.py +++ b/coursebuilder/modules/gatherings/messages.py @@ -6,8 +6,12 @@ This is the message of the gathering. """ -GATHERING_DATE_DESCRIPTION = """ -This places a date on your gathering. +GATHERING_START_TIME_DESCRIPTION = """ +Start time of your Gathering. +""" + +GATHERING_END_TIME_DESCRIPTION = """ +End time of your Gathering. """ GATHERING_STATUS_DESCRIPTION = """ diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index 897e5c29..e122b8d4 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -21,13 +21,15 @@ mutateGatheringParticipation('join', gid, btn, function() { console.log($('#leave_'+ gid)); $('#leave_'+ gid).show(); + $('#vc_'+ gid).show(); }); } function leaveGathering(gid, btn) { mutateGatheringParticipation('leave', gid, btn, function() { console.log($('join_'+ gid)); - $('#join_'+ gid).show() + $('#join_'+ gid).show(); + $('#vc_'+ gid).hide(); }); } function mutateGatheringParticipation(action, gid, btn, cb) { @@ -75,6 +77,7 @@

    {{ item.title }} {% if item.is_draft %}(Private){% endif %} + {{item.status}} {% if item.edit_action %} {% if not item.joined %} style="display: none;"{% endif %}> Leave + {% if item.vc_url %} + + {% endif %} +

    {% endfor %} {% else %} From 25184d084450f97a51aac70ab45add40b2e3b5bf Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 13:14:49 -0700 Subject: [PATCH 06/12] Pretty Date --- coursebuilder/modules/gatherings/gatherings.py | 15 +++++++++++++-- coursebuilder/views/gatherings.html | 3 +-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index 333cee27..fe84685f 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -118,15 +118,26 @@ def format_items_for_template(self, items, user=None): if user: item['joined'] = item_entitiy.key() in joined_gatherings now = utc.now_as_datetime() - if now < item_entitiy.start_time: + start_time = item_entitiy.start_time + end_time = item_entitiy.end_time + if now < start_time: item['status'] = 'future' del item['vc_url'] - elif now > item_entitiy.end_time: + elif now > end_time: item['status'] = 'past' del item['vc_url'] else: item['status'] = 'present' + start_fmt = '%d %B' + \ + (' %Y' if start_time.year != now.year else '') + \ + ' %I:%M %p %Z' + + end_fmt = '%I:%M %p %Z' \ + if start_time.date() == end_time.date() else start_fmt + item['start_time'] = start_time.strftime(start_fmt) + item['end_time'] = end_time.strftime(end_fmt) + # add 'edit' actions if GatheringsRights.can_edit(self): item['edit_action'] = self.get_gathering_action_url( diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index e122b8d4..6b628af8 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -84,8 +84,7 @@

    class="gcb-edit-resource-button icon material-icons">edit {% endif %}

    -

    Start Time: {{item.start_time}}

    -

    End Time: {{item.end_time}}

    +

    {{item.start_time}} - {{item.end_time}}

    {# TODO (chaks): Change the class #}

    From 0cd23a56f8de286873706f2782a9d4b2c4c829ca Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 14:35:47 -0700 Subject: [PATCH 07/12] Upcoming, Past, Ongoing gathering: Order gatherings, put tag on it --- coursebuilder/assets/css/main.css | 24 ++++ .../modules/gatherings/gatherings.py | 9 +- coursebuilder/views/gatherings.html | 106 ++++++++++-------- 3 files changed, 87 insertions(+), 52 deletions(-) diff --git a/coursebuilder/assets/css/main.css b/coursebuilder/assets/css/main.css index ec58aff8..64a6c52b 100644 --- a/coursebuilder/assets/css/main.css +++ b/coursebuilder/assets/css/main.css @@ -398,6 +398,30 @@ a.gcb-button-action, button.gcb-button-action { border: 1px solid #009A5D; } +.gcb-gathering { + line-height: 1; +} + +.gcb-past-gathering { + opacity: 0.5; +} +.gcb-gathering-status { + color: white; + font-weight: normal; + border-radius: 0.2em; + padding: 0.3em 0.4em; + margin-left: 2px; +} +.gcb-gathering-status.gcb-gathering-status-past { + background-color: #555; +} +.gcb-gathering-status.gcb-gathering-status-upcoming { + background-color: hsl(220, 18%, 45%); +} +.gcb-gathering-status.gcb-gathering-status-ongoing { + background-color: rgba(236, 65, 65, 0.81); +} + #gcb-nav-x { background-color: #4D5B76; background-image: linear-gradient(top, #6c7a95, #4d5b76); diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index fe84685f..84c00c67 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -121,13 +121,12 @@ def format_items_for_template(self, items, user=None): start_time = item_entitiy.start_time end_time = item_entitiy.end_time if now < start_time: - item['status'] = 'future' + item['status'] = 'upcoming' del item['vc_url'] elif now > end_time: item['status'] = 'past' - del item['vc_url'] else: - item['status'] = 'present' + item['status'] = 'ongoing' start_fmt = '%d %B' + \ (' %Y' if start_time.year != now.year else '') + \ @@ -152,6 +151,8 @@ def format_items_for_template(self, items, user=None): template_items.append(item) output = {} + status_order = ['ongoing', 'upcoming', 'past'] + template_items = [i for status in status_order for i in template_items if i['status'] == status] output['children'] = template_items # add 'add' action @@ -324,7 +325,7 @@ def SCHEMA(cls): 'excludedCustomTags': tags.EditorBlacklists.COURSE_SCOPE}, optional=True)) schema.add_property(schema_fields.SchemaField( - 'vc_url', 'VC URL', 'url', editable=False, + 'vc_url', 'VC URL', 'string', editable=False, description='Link to Video Conference')) schema.add_property(schema_fields.SchemaField( 'start_time', 'Start Time', 'datetime', diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index 6b628af8..fa9ca7f0 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -47,7 +47,7 @@ }

    -
    +
    {% if gatherings %} {% if gatherings.add_action %} @@ -70,56 +70,66 @@

    {% endif %} {% for item in gatherings.children %} -
    -

    - - {# TODO (chaks): Change the class #} - - {{ item.title }} - {% if item.is_draft %}(Private){% endif %} - {{item.status}} - - {% if item.edit_action %} - edit - {% endif %} -

    -

    {{item.start_time}} - {{item.end_time}}

    +
    +
    +

    + + {# TODO (chaks): Change the class #} + + {{ item.title }} + {% if item.is_draft %}(Private){% endif %} - {# TODO (chaks): Change the class #} -

    - {{ item.html | gcb_tags }} -

    -

    - - - {% if item.vc_url %} - edit + {% endif %} +

    +

    + {{item.start_time}} - {{item.end_time}} + + {{item.status|title}} + +

    + + {# TODO (chaks): Change the class #} +

    + {{ item.html | gcb_tags }} +

    +

    - - {% endif %} - -

    + + {% if item.vc_url %} + + {% endif %} +

    +
    {% endfor %} {% else %} {{ content }} From 96f1cb8489600bd1099ceedc2937493d4a741683 Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 14:44:58 -0700 Subject: [PATCH 08/12] Couple of Improvements to Gathering buttons 1. Archived VC link for past gatherings 2. Coming soon disabled VC link for future gatherings 3. Disable Join/Leave links for past gatherings --- coursebuilder/views/gatherings.html | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index fa9ca7f0..ebb67c2d 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -103,6 +103,7 @@

    - {% if item.vc_url %} - {% endif %}

    {% endfor %} From 485d34532e6f1104f61f1e0b2aa149d07acfb65c Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 15:53:23 -0700 Subject: [PATCH 09/12] Start time should always be earlier than end time --- coursebuilder/modules/gatherings/gatherings.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index 84c00c67..fa47192c 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -396,7 +396,7 @@ def post(self): return user = self.get_user() - # TODO (chaks) Check for if user is a student + # Check if already a participants existing = GatheringsUsersEntity.all().filter( 'user =', @@ -448,6 +448,18 @@ def put(self): payload = request.get('payload') update_dict = transforms.json_to_dict( transforms.loads(payload), schema.get_json_schema_dict()) + + if update_dict['start_time'] > update_dict['end_time']: + transforms.send_json_response( + self, + 400, + 'End time is earlier than Start time', + { + 'start_time': str(update_dict['start_time']), + 'end_time': str(update_dict['end_time']), + } + ) + return if entity.is_draft and not update_dict.get('set_draft'): item = news.NewsItem( str(TranslatableResourceGathering.key_for_entity(entity)), From cb23579432fe7dc4e4f991a6d9ac63033427483d Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 16:12:20 -0700 Subject: [PATCH 10/12] Gatherings: Finish remaining todos 1. Dehardcode the REST URL 2. Remove non-existant announcement classes --- .../modules/gatherings/gatherings.py | 7 +-- coursebuilder/views/gatherings.html | 52 ++++++++----------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index fa47192c..daed33e7 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -194,6 +194,8 @@ def get_list(self): items, self.get_user(), ) + self.template_value['gatherings_post_url'] = \ + GatheringsItemRESTHandler.URL.lstrip('/') self._render() def _render(self): @@ -248,7 +250,7 @@ def get_edit_gathering(self): schema = GatheringsItemRESTHandler.SCHEMA() exit_url = self.canonicalize_url('/{}'.format(self.LIST_URL)) - rest_url = self.canonicalize_url('/rest/gatherings/item') + rest_url = self.canonicalize_url(GatheringsItemRESTHandler.URL) form_html = oeditor.ObjectEditor.get_html_for( self, schema.get_json_schema(), @@ -301,9 +303,8 @@ def post_add_gathering(self): class GatheringsItemRESTHandler(utils.BaseRESTHandler): """Provides REST API for an gathering.""" - #TODO (chaks): Dont like the /item here - URL = '/rest/gatherings/item' + URL = '/rest/gatherings/item' ACTION = 'gathering-put' STATUS_ACTION = 'set_draft_status_gathering' diff --git a/coursebuilder/views/gatherings.html b/coursebuilder/views/gatherings.html index ebb67c2d..c4f011c6 100644 --- a/coursebuilder/views/gatherings.html +++ b/coursebuilder/views/gatherings.html @@ -34,8 +34,7 @@ } function mutateGatheringParticipation(action, gid, btn, cb) { $.ajax({ - // TODO (chaks): Dehardcode this URL - url: "rest/gatherings/item", + url: "{{ gatherings_post_url }}", type: "POST", data: {key: gid, action: action}, dataType: "text", @@ -64,10 +63,7 @@ {% endif %} {% if not gatherings.children %} {# I18N: Shown if the list of gatherings is empty. #} - {# TODO (chaks): Change the class #} -

    - {{ gettext('Currently, there are no gatherings.') }} -

    +

    {{ gettext('Currently, there are no gatherings.') }}

    {% endif %} {% for item in gatherings.children %}

    - - {# TODO (chaks): Change the class #} - - {{ item.title }} - {% if item.is_draft %}(Private){% endif %} - + + {{ item.title }} {% if item.is_draft %}(Private){% endif %} {% if item.edit_action %} {{item.status|title}}

    - - {# TODO (chaks): Change the class #} -

    +

    {{ item.html | gcb_tags }}

    - - + +

    {% endfor %} From 7da27792900b8cf2a6eb66d85cefffd55712113f Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 21:14:51 -0700 Subject: [PATCH 11/12] Uncomment gathering_list.html from manifest --- coursebuilder/modules/gatherings/manifest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coursebuilder/modules/gatherings/manifest.yaml b/coursebuilder/modules/gatherings/manifest.yaml index 1aa9fa4d..6ec5e76c 100644 --- a/coursebuilder/modules/gatherings/manifest.yaml +++ b/coursebuilder/modules/gatherings/manifest.yaml @@ -9,6 +9,6 @@ files: - modules/gatherings/__init__.py - modules/gatherings/gatherings.py # - modules/gatherings/gatherings_tests.py -# - modules/gatherings/templates/gathering_list.html + - modules/gatherings/templates/gathering_list.html - modules/gatherings/manifest.yaml - modules/gatherings/messages.py From bc58523091bc91bd3ca224558d3f525077c800e6 Mon Sep 17 00:00:00 2001 From: Chakshu Ahuja Date: Sun, 3 Sep 2017 21:18:58 -0700 Subject: [PATCH 12/12] Nits: Changed author and comments in gathering.py --- coursebuilder/modules/gatherings/gatherings.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/coursebuilder/modules/gatherings/gatherings.py b/coursebuilder/modules/gatherings/gatherings.py index daed33e7..d3c3d5e6 100644 --- a/coursebuilder/modules/gatherings/gatherings.py +++ b/coursebuilder/modules/gatherings/gatherings.py @@ -14,7 +14,7 @@ """Classes and methods to create and manage Gatherings.""" -__author__ = 'Saifu Angto (saifu@google.com)' +__author__ = 'Chakshu Ahuja (ahuja.chaks@gmail.com)' import cgi @@ -553,13 +553,9 @@ def post_set_draft_status(cls, handler): class GatheringEntity(entities.BaseEntity): - """A class that represents a persistent database entity of gatherings. - - Note that this class was added to Course Builder prior to the idioms - introduced in models.models.BaseJsonDao and friends. That being the - case, this class is much more hand-coded and not well integrated into - the structure of callbacks and hooks that have accumulated around - entity caching, i18n, and the like. + """ + A class that represents a persistent database entity of gatherings. + This should be moved to models """ title = db.StringProperty(indexed=False)