diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 472bef7dac..06c59d7937 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -40,7 +40,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from datetime import date from collections import namedtuple -from courseware.courses import get_courses_by_university +from courseware.courses import get_courses from courseware.access import has_access from statsd import statsd @@ -74,16 +74,21 @@ def index(request, extra_context={}, user=None): domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False if domain==False: # do explicit check, because domain=None is valid domain = request.META.get('HTTP_HOST') - universities = get_courses_by_university(None, - domain=domain) + + courses = get_courses(None, domain=domain) + + # Sort courses by how far are they from they start day + key = lambda course: course.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) # Get the 3 most recent news top_news = _get_news(top=3) - context = {'universities': universities, 'news': top_news} + context = {'courses': courses, 'news': top_news} context.update(extra_context) return render_to_response('index.html', context) + def course_from_id(course_id): """Return the CourseDescriptor corresponding to this course_id""" course_loc = CourseDescriptor.id_to_location(course_id) diff --git a/common/djangoapps/track/migrations/0001_initial.py b/common/djangoapps/track/migrations/0001_initial.py new file mode 100644 index 0000000000..0546203cf8 --- /dev/null +++ b/common/djangoapps/track/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'TrackingLog' + db.create_table('track_trackinglog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('dtcreated', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('username', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('ip', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('event_source', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('event_type', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('event', self.gf('django.db.models.fields.TextField')(blank=True)), + ('agent', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)), + ('page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('track', ['TrackingLog']) + + + def backwards(self, orm): + # Deleting model 'TrackingLog' + db.delete_table('track_trackinglog') + + + models = { + 'track.trackinglog': { + 'Meta': {'object_name': 'TrackingLog'}, + 'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), + 'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'event_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'page': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}) + } + } + + complete_apps = ['track'] \ No newline at end of file diff --git a/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py b/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py new file mode 100644 index 0000000000..4c73aa3bfd --- /dev/null +++ b/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'TrackingLog.host' + db.add_column('track_trackinglog', 'host', + self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True), + keep_default=False) + + + # Changing field 'TrackingLog.event_type' + db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=512)) + + # Changing field 'TrackingLog.page' + db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=512, null=True)) + + def backwards(self, orm): + # Deleting field 'TrackingLog.host' + db.delete_column('track_trackinglog', 'host') + + + # Changing field 'TrackingLog.event_type' + db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=32)) + + # Changing field 'TrackingLog.page' + db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)) + + models = { + 'track.trackinglog': { + 'Meta': {'object_name': 'TrackingLog'}, + 'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), + 'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'event_type': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'page': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}) + } + } + + complete_apps = ['track'] \ No newline at end of file diff --git a/common/djangoapps/track/migrations/__init__.py b/common/djangoapps/track/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/track/models.py b/common/djangoapps/track/models.py index 401fa2832f..dfdf7a0558 100644 --- a/common/djangoapps/track/models.py +++ b/common/djangoapps/track/models.py @@ -7,11 +7,12 @@ class TrackingLog(models.Model): username = models.CharField(max_length=32,blank=True) ip = models.CharField(max_length=32,blank=True) event_source = models.CharField(max_length=32) - event_type = models.CharField(max_length=32,blank=True) + event_type = models.CharField(max_length=512,blank=True) event = models.TextField(blank=True) agent = models.CharField(max_length=256,blank=True) - page = models.CharField(max_length=32,blank=True,null=True) + page = models.CharField(max_length=512,blank=True,null=True) time = models.DateTimeField('event time') + host = models.CharField(max_length=64,blank=True) def __unicode__(self): s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source, diff --git a/common/djangoapps/track/views.py b/common/djangoapps/track/views.py index 434e75a63f..54bd476799 100644 --- a/common/djangoapps/track/views.py +++ b/common/djangoapps/track/views.py @@ -17,7 +17,7 @@ from track.models import TrackingLog log = logging.getLogger("tracking") -LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time'] +LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host'] def log_event(event): event_str = json.dumps(event) @@ -58,6 +58,7 @@ def user_track(request): "agent": agent, "page": request.GET['page'], "time": datetime.datetime.utcnow().isoformat(), + "host": request.META['SERVER_NAME'], } log_event(event) return HttpResponse('success') @@ -83,6 +84,7 @@ def server_track(request, event_type, event, page=None): "agent": agent, "page": page, "time": datetime.datetime.utcnow().isoformat(), + "host": request.META['SERVER_NAME'], } if event_type.startswith("/event_logs") and request.user.is_staff: # don't log diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 65a1eee25b..dc530bdebc 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -217,11 +217,51 @@ def get_courses_by_university(user, domain=None): ''' # TODO: Clean up how 'error' is done. # filter out any courses that errored. - visible_courses = branding.get_visible_courses(domain) + visible_courses = get_courses(user, domain) universities = defaultdict(list) for course in visible_courses: - if not has_access(user, course, 'see_exists'): - continue universities[course.org].append(course) + return universities + + +def get_courses(user, domain=None): + ''' + Returns a list of courses available, sorted by course.number + ''' + courses = branding.get_visible_courses(domain) + courses = [c for c in courses if has_access(user, c, 'see_exists')] + + # Add metadata about the start day and if the course is new + for course in courses: + days_to_start = _get_course_days_to_start(course) + + metadata = course.metadata + metadata['days_to_start'] = days_to_start + metadata['is_new'] = course.metadata.get('is_new', days_to_start > 1) + + courses = sorted(courses, key=lambda course:course.number) + return courses + + +def _get_course_days_to_start(course): + from datetime import datetime as dt + from time import mktime, gmtime + + convert_to_datetime = lambda ts: dt.fromtimestamp(mktime(ts)) + + start_date = convert_to_datetime(course.start) + + # If the course has a valid advertised date, use that instead + advertised_start = course.metadata.get('advertised_start', None) + if advertised_start: + try: + start_date = dt.strptime(advertised_start, "%Y-%m-%dT%H:%M") + except ValueError: + pass # Invalid date, keep using course.start + + now = convert_to_datetime(gmtime()) + days_to_start = (start_date - now).days + + return days_to_start diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 276af80ca9..f6e87dfe9f 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -17,7 +17,7 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access -from courseware.courses import (get_course_with_access, get_courses_by_university) +from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university) import courseware.tabs as tabs from courseware.models import StudentModuleCache from module_render import toc_for_course, get_module, get_instance_module @@ -61,16 +61,19 @@ def user_groups(user): return group_names - @ensure_csrf_cookie @cache_if_anonymous def courses(request): ''' Render "find courses" page. The course selection work is done in courseware.courses. ''' - universities = get_courses_by_university(request.user, - domain=request.META.get('HTTP_HOST')) - return render_to_response("courseware/courses.html", {'universities': universities}) + courses = get_courses(request.user, domain=request.META.get('HTTP_HOST')) + + # Sort courses by how far are they from they start day + key = lambda course: course.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) + + return render_to_response("courseware/courses.html", {'courses': courses}) def render_accordion(request, course, chapter, section): @@ -317,7 +320,7 @@ def jump_to(request, course_id, location): except NoPathToItem: raise Http404("This location is not in any class: {0}".format(location)) - # choose the appropriate view (and provide the necessary args) based on the + # choose the appropriate view (and provide the necessary args) based on the # args provided by the redirect. # Rely on index to do all error handling and access control. if chapter is None: @@ -328,7 +331,7 @@ def jump_to(request, course_id, location): return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section) else: return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position) - + @ensure_csrf_cookie def course_info(request, course_id): """ @@ -435,6 +438,11 @@ def university_profile(request, org_id): # Only grab courses for this org... courses = get_courses_by_university(request.user, domain=request.META.get('HTTP_HOST'))[org_id] + + # Sort courses by how far are they from they start day + key = lambda course: course.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) + context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() diff --git a/lms/envs/test.py b/lms/envs/test.py index ef2a343db4..c72c8b98bf 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -44,12 +44,6 @@ STATUS_MESSAGE_PATH = TEST_ROOT / "status_message.json" COURSES_ROOT = TEST_ROOT / "data" DATA_DIR = COURSES_ROOT -LOGGING = get_logger_config(TEST_ROOT / "log", - logging_env="dev", - tracking_filename="tracking.log", - dev_env=True, - debug=True) - COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # Where the content data is checked out. This may not exist on jenkins. GITHUB_REPO_ROOT = ENV_ROOT / "data" diff --git a/lms/static/sass/shared/_course_object.scss b/lms/static/sass/shared/_course_object.scss index 374caf4898..2c638ed158 100644 --- a/lms/static/sass/shared/_course_object.scss +++ b/lms/static/sass/shared/_course_object.scss @@ -13,6 +13,23 @@ } } + .courses-listing { + @include clearfix(); + margin: 0; + padding: 0; + list-style: none; + + .courses-listing-item { + width: flex-grid(4); + margin-right: flex-gutter(); + float: left; + + &:nth-child(3n+3) { + margin-right: 0; + } + } + } + .course { background: rgb(250,250,250); border: 1px solid rgb(180,180,180); @@ -24,6 +41,31 @@ width: 100%; @include transition(all, 0.15s, linear); + .status { + background: $blue; + color: white; + font-size: 10px; + left: 10px; + padding: 2px 10px; + @include border-radius(2px); + position: absolute; + text-transform: uppercase; + top: -6px; + z-index: 100; + } + + .status:after { + border-bottom: 6px solid shade($blue, 50%); + border-right: 6px solid transparent; + content: ""; + display: block; + height: 0; + position: absolute; + right: -6px; + top: 0; + width: 0; + } + a:hover { text-decoration: none; } diff --git a/lms/templates/course.html b/lms/templates/course.html index 50a00f9d31..a3217d2da5 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -5,6 +5,9 @@ %> <%page args="course" />
+ %if course.metadata.get('is_new'): + New + %endif
diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 0c45faa923..a8fe851d19 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -20,21 +20,13 @@ ## I'm removing this for now since we aren't using it for the fall. ## <%include file="course_filter.html" />
-
- %for course in universities['MITx']: +
    + %for course in courses: +
  • <%include file="../course.html" args="course=course" /> +
  • %endfor -
-
- %for course in universities['HarvardX']: - <%include file="../course.html" args="course=course" /> - %endfor -
-
- %for course in universities['BerkeleyX']: - <%include file="../course.html" args="course=course" /> - %endfor -
+
diff --git a/lms/templates/index.html b/lms/templates/index.html index ca15eeae81..d82c9120d4 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -106,21 +106,13 @@
-
- %for course in universities['MITx']: - <%include file="course.html" args="course=course" /> +
    + %for course in courses: +
  • + <%include file="course.html" args="course=course" /> +
  • %endfor -
-
- %for course in universities['HarvardX']: - <%include file="course.html" args="course=course" /> - %endfor -
-
- %for course in universities['BerkeleyX']: - <%include file="course.html" args="course=course" /> - %endfor -
+
diff --git a/rakefile b/rakefile index 3828ad036d..53d7381dd7 100644 --- a/rakefile +++ b/rakefile @@ -169,7 +169,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| TEST_TASK_DIRS << lib desc "Run tests for common lib #{lib} (without coverage)" - task "fasttest_#{lib}" do + task "fasttest_#{lib}" do sh("nosetests #{lib}") end @@ -309,16 +309,22 @@ task :builddocs do end end -desc "Show doc in browser (mac only for now) TODO add linux support" +desc "Show docs in browser (mac and ubuntu)." task :showdocs do Dir.chdir('docs/build/html') do - sh('open index.html') + if RUBY_PLATFORM.include? 'darwin' # mac os + sh('open index.html') + elsif RUBY_PLATFORM.include? 'linux' # make more ubuntu specific? + sh('sensible-browser index.html') # ubuntu + else + raise "\nUndefined how to run browser on your machine. +Please use 'rake builddocs' and then manually open +'mitx/docs/build/html/index.html." + end end end desc "Build docs and show them in browser" task :doc => :builddocs do - Dir.chdir('docs/build/html') do - sh('open index.html') - end + Rake::Task["showdocs"].invoke end