def mark_item_seen(self, resource_key): now = utc.now_as_datetime() # First, add/update a record to indicate that the student has just now # seen the newsworthy thing. # Note: Using OrderedDict's here because they permit deletion during # iteration. seen_items = collections.OrderedDict( {i.resource_key: i for i in self.get_seen_items()}) seen_items[resource_key] = SeenItem(resource_key, now) # As long as we're here, also take this opportunity to clean up: # Remove pairs of items where we have a 'seen' record and a 'news' # record for the same key and where the item was seen more than # NEWSWORTHINESS_SECONDS ago. We retain things that are only # slightly-old so that students can still use the News feature to # re-find stuff they've already seen but may still want to re-visit. news_items = collections.OrderedDict( {n.resource_key: n for n in self.get_news_items()}) for resource_key, seen_item in seen_items.iteritems(): if (now - seen_item.when).total_seconds() > NEWSWORTHINESS_SECONDS: if resource_key in news_items: del news_items[resource_key] del seen_items[resource_key] break self._set_seen_items(seen_items.values()) self._set_news_items(news_items.values())
def __init__(self, resource_key, url, when=None, labels=None): # String version of common.resource.Key self.resource_key = resource_key # The time when this item became news. self.when = when or utc.now_as_datetime() # URL to the page showing the item. self.url = url # Single string giving IDs of labels, whitespace separated. Same as # labels field on Student, Announcement, Unit and so on. Used to # restrict news on items that are labelled to only students with # matching labels. Follows usual label-match rules: if either Student # or NewsItem does not have labels in a category, category does not # filter. If both have labels, at least one label must exist in # common for match. self.labels = labels or '' # -------------------------------------------------------------------- # Below here is transient data - not persisted. Overwritten only for # UX display. Note that since the serialization library ignores # transient items based on leading-underscore, we also provide # getter/setter properties to avoid warnings about touching private # members. # Distinguish news items that are likely interesting versus items that # are likely old news for the student. self._is_new_news = None # Title, suitably i18n'd for the current display locale. self._i18n_title = None
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 run(self): now = utc.now_as_datetime() namespace = namespace_manager.get_namespace() app_context = sites.get_app_context_for_namespace(namespace) course = courses.Course.get(app_context) env = app_context.get_environ() tct = triggers.ContentTrigger content_acts = tct.act_on_settings(course, env, now) tmt = triggers.MilestoneTrigger course_acts = tmt.act_on_settings(course, env, now) save_settings = content_acts.num_consumed or course_acts.num_consumed if save_settings: # At least one of the settings['publish'] triggers was consumed # or discarded, so save changes to triggers into the settings. settings_saved = course.save_settings(env) else: settings_saved = False save_course = content_acts.num_changed or course_acts.num_changed if save_course: course.save() tct.log_acted_on( namespace, content_acts, save_course, settings_saved) tmt.log_acted_on( namespace, course_acts, save_course, settings_saved) common_utils.run_hooks(self.RUN_HOOKS.itervalues(), course)
def run(self): now = utc.now_as_datetime() namespace = namespace_manager.get_namespace() app_context = sites.get_app_context_for_namespace(namespace) course = courses.Course.get(app_context) env = app_context.get_environ() tct = triggers.ContentTrigger content_acts = tct.act_on_settings(course, env, now) tmt = triggers.MilestoneTrigger course_acts = tmt.act_on_settings(course, env, now) save_settings = content_acts.num_consumed or course_acts.num_consumed if save_settings: # At least one of the settings['publish'] triggers was consumed # or discarded, so save changes to triggers into the settings. settings_saved = course.save_settings(env) else: settings_saved = False save_course = content_acts.num_changed or course_acts.num_changed if save_course: course.save() tct.log_acted_on(namespace, content_acts, save_course, settings_saved) tmt.log_acted_on(namespace, course_acts, save_course, settings_saved) common_utils.run_hooks(self.RUN_HOOKS.itervalues(), course)
def _test_add_duplicate_news_item(self, dao_class): now = utc.now_as_datetime() news_item = news.NewsItem('test:key', 'test:url', when=now) dao_class.add_news_item(news_item) dao_class.add_news_item(news_item) dao_class.add_news_item(news_item) dao_class.add_news_item(news_item) self.assertEquals([news_item], dao_class.get_news_items())
def _test_mark_item_seen(self, dao_class): actions.login(self.STUDENT_EMAIL) actions.register(self, 'John Smith') now = utc.now_as_datetime() news_item = news.NewsItem('test:key', 'test:url', when=now) dao_class.add_news_item(news_item) self.assertEquals([news_item], dao_class.get_news_items()) news.StudentNewsDao.mark_item_seen(news_item.resource_key) seen_item = news.SeenItem(news_item.resource_key, now) self.assertEquals([seen_item], news.StudentNewsDao.get_seen_items()) # Move time forward one tick, and verify that the timestamp *does* # move forward on the 'seen' record. time.sleep(1) seen_item.when = utc.now_as_datetime() news.StudentNewsDao.mark_item_seen(news_item.resource_key) self.assertEquals([seen_item], news.StudentNewsDao.get_seen_items())
def test_old_student_news_removed_when_seen_far_in_the_past(self): actions.login(self.STUDENT_EMAIL) actions.register(self, 'John Smith') now = utc.now_as_datetime() item_one = news.NewsItem('key_one', 'test:url', when=now) item_two = news.NewsItem('key_two', 'test:url', when=now) news.StudentNewsDao.add_news_item(item_one) news.StudentNewsDao.add_news_item(item_two) # Nothing seen; should have both news items still. news_items = news.StudentNewsDao.get_news_items() self.assertEquals(2, len(news_items)) self.assertIn(item_one, news_items) self.assertIn(item_two, news_items) # Now we mark item_one as visited. Still should retain both items, # since we're within the newsworthiness time limit. news.StudentNewsDao.mark_item_seen(item_one.resource_key) seen_one = news.SeenItem(item_one.resource_key, now) self.assertEquals([seen_one], news.StudentNewsDao.get_seen_items()) news_items = news.StudentNewsDao.get_news_items() self.assertEquals(2, len(news_items)) self.assertIn(item_one, news_items) self.assertIn(item_two, news_items) # Set the newsworthiness timeout to one second so we can get this # done in a sane amount of time. try: save_newsworthiness_seconds = news.NEWSWORTHINESS_SECONDS news.NEWSWORTHINESS_SECONDS = 1 time.sleep(2) now = utc.now_as_datetime() # Marking item two as seen should have the side effect of # removing the seen and news items for 'key_one'. news.StudentNewsDao.mark_item_seen(item_two.resource_key) self.assertEquals([item_two], news.StudentNewsDao.get_news_items()) seen_two = news.SeenItem(item_two.resource_key, now) self.assertEquals([seen_two], news.StudentNewsDao.get_seen_items()) finally: news.NEWSWORTHINESS_SECONDS = save_newsworthiness_seconds
def test_news_before_user_registration_is_not_news(self): news_item = news.NewsItem( 'test:before', 'before_url', utc.now_as_datetime()) news.CourseNewsDao.add_news_item(news_item) time.sleep(1) user = actions.login(self.STUDENT_EMAIL) actions.register(self, 'John Smith') now = utc.now_as_datetime() news_item = news.NewsItem('test:at', 'at_url', now) self._set_student_enroll_date(user, now) news.CourseNewsDao.add_news_item(news_item) time.sleep(1) news_item = news.NewsItem( 'test:after', 'after_url', utc.now_as_datetime()) news.CourseNewsDao.add_news_item(news_item) # Expect to not see news item from before student registration. response = self.get('course') soup = self.parse_html_string_to_soup(response.body) self.assertEquals( [news_tests_lib.NewsItem('Test Item after', 'after_url', True), news_tests_lib.NewsItem('Test Item at', 'at_url', True)], news_tests_lib.extract_news_items_from_soup(soup))
def _test_remove_news_item(self, dao_class): # Just need to see no explosions. dao_class.remove_news_item('no_such_item_key') # Add an item so that we can remove it next. now = utc.now_as_datetime() news_item = news.NewsItem('test:key', 'test:url', when=now) dao_class.add_news_item(news_item) self.assertEquals([news_item], dao_class.get_news_items()) # Remove the item; verify empty set of items. dao_class.remove_news_item(news_item.resource_key) self.assertEquals([], dao_class.get_news_items()) # Remove again an item that had existed; just want no explosions. dao_class.remove_news_item(news_item.resource_key) self.assertEquals([], dao_class.get_news_items())
def course_page_navbar_callback(app_context): """Generate HTML for inclusion on tabs bar. Thankfully, this function gets called pretty late during page generation, so StudentNewsDao should already have been notified when we're on a page that was newsworthy, but now is not because the student has seen it. """ # If we don't have a registered student in session, no news for you! user = users.get_current_user() if not user: return [] student = models.Student.get_enrolled_student_by_user(user) if not student or student.is_transient: return [] student_dao = StudentNewsDao.load_or_default() # Combine all news items for consideration. news = student_dao.get_news_items() + CourseNewsDao.get_news_items() seen_times = {s.resource_key: s.when for s in student_dao.get_seen_items()} # Filter out items that student can't see due to label matching. Do # this before reducing number of items displayed to a fixed maximum. course = courses.Course.get(app_context) models.LabelDAO.apply_course_track_labels_to_student_labels( course, student, news) # Run through news items, categorizing 'new' and 'old' news for display. # news is everything else. new_news = [] old_news = [] now = utc.now_as_datetime() enrolled_on = student.enrolled_on.replace(microsecond=0) for item in news: seen_when = seen_times.get(item.resource_key) if seen_when is None: # Items not yet seen at all get marked for CSS highlighting. # Items prior to student enrollment are not incremental new stuff; # we assume that on enroll, the student is on notice that all # course content is "new", and we don't need to redundantly bring # it to their attention. if item.when >= enrolled_on: item.is_new_news = True new_news.append(item) elif (now - seen_when).total_seconds() < NEWSWORTHINESS_SECONDS: # Items seen recently are always shown, but with CSS dimming. item.is_new_news = False new_news.append(item) else: # Items seen and not recently are put on seprate list for # inclusion only if there are few new items. item.is_new_news = False old_news.append(item) # Display setup: Order by time within new, old set. Show all new # news, and if there are few of those, some old news as well. new_news.sort(key=lambda n: (n.is_new_news, n.when), reverse=True) old_news.sort(key=lambda n: n.when, reverse=True) news = new_news + old_news[0:max(0, MIN_NEWS_ITEMS_TO_DISPLAY - len(new_news))] for item in news: try: key = resource.Key.fromstring(item.resource_key) resource_handler = ( i18n_dashboard.TranslatableResourceRegistry.get_by_type( key.type)) item.i18n_title = resource_handler.get_i18n_title(key) except AssertionError: # Not all news things are backed by AbstractResourceHandler types. # Fall back to news-specific registry for these. resource_handler = I18nTitleRegistry key_type, _ = item.resource_key.split(resource.Key.SEPARATOR, 1) item.i18n_title = resource_handler.get_i18n_title( key_type, item.resource_key) # Fill template template_environ = app_context.get_template_environ( app_context.get_current_locale(), [TEMPLATES_DIR]) template = template_environ.get_template('news.html', [TEMPLATES_DIR]) return [ jinja2.utils.Markup(template.render({'news': news}, autoescape=True)) ]
def _new_course_counts(app_context, unused_errors): """Called back from CoursesItemRESTHandler when new course is created.""" namespace_name = app_context.get_namespace_name() TotalEnrollmentDAO.set(namespace_name, 0) EnrollmentsAddedDAO.set(namespace_name, utc.now_as_datetime(), 0) EnrollmentsDroppedDAO.set(namespace_name, utc.now_as_datetime(), 0)
def course_page_navbar_callback(app_context): """Generate HTML for inclusion on tabs bar. Thankfully, this function gets called pretty late during page generation, so StudentNewsDao should already have been notified when we're on a page that was newsworthy, but now is not because the student has seen it. """ # If we don't have a registered student in session, no news for you! user = users.get_current_user() if not user: return [] student = models.Student.get_enrolled_student_by_user(user) if not student or student.is_transient: return [] student_dao = StudentNewsDao.load_or_default() # Combine all news items for consideration. news = student_dao.get_news_items() + CourseNewsDao.get_news_items() seen_times = {s.resource_key: s.when for s in student_dao.get_seen_items()} # Filter out items that student can't see due to label matching. Do # this before reducing number of items displayed to a fixed maximum. course = courses.Course.get(app_context) models.LabelDAO.apply_course_track_labels_to_student_labels( course, student, news) # Run through news items, categorizing 'new' and 'old' news for display. # news is everything else. new_news = [] old_news = [] now = utc.now_as_datetime() enrolled_on = student.enrolled_on.replace(microsecond=0) for item in news: seen_when = seen_times.get(item.resource_key) if seen_when is None: # Items not yet seen at all get marked for CSS highlighting. # Items prior to student enrollment are not incremental new stuff; # we assume that on enroll, the student is on notice that all # course content is "new", and we don't need to redundantly bring # it to their attention. if item.when >= enrolled_on: item.is_new_news = True new_news.append(item) elif (now - seen_when).total_seconds() < NEWSWORTHINESS_SECONDS: # Items seen recently are always shown, but with CSS dimming. item.is_new_news = False new_news.append(item) else: # Items seen and not recently are put on seprate list for # inclusion only if there are few new items. item.is_new_news = False old_news.append(item) # Display setup: Order by time within new, old set. Show all new # news, and if there are few of those, some old news as well. new_news.sort(key=lambda n: (n.is_new_news, n.when), reverse=True) old_news.sort(key=lambda n: n.when, reverse=True) news = new_news + old_news[ 0:max(0, MIN_NEWS_ITEMS_TO_DISPLAY - len(new_news))] for item in news: try: key = resource.Key.fromstring(item.resource_key) resource_handler = ( i18n_dashboard.TranslatableResourceRegistry.get_by_type( key.type)) item.i18n_title = resource_handler.get_i18n_title(key) except AssertionError: # Not all news things are backed by AbstractResourceHandler types. # Fall back to news-specific registry for these. resource_handler = I18nTitleRegistry key_type, _ = item.resource_key.split(resource.Key.SEPARATOR, 1) item.i18n_title = resource_handler.get_i18n_title( key_type, item.resource_key) # Fill template template_environ = app_context.get_template_environ( app_context.get_current_locale(), [TEMPLATES_DIR]) template = template_environ.get_template('news.html', [TEMPLATES_DIR]) return [ jinja2.utils.Markup(template.render({'news': news}, autoescape=True))]