Skip to content

Commit 42dddf6

Browse files
authored
Merge pull request #242 from appsembler/john/update-monthly-task
John/update monthly task
2 parents 33e61a3 + 4a27cb9 commit 42dddf6

14 files changed

+239
-51
lines changed

devsite/requirements/ginkgo.txt

+7-3
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,16 @@ recommonmark==0.4.0
5050
##
5151

5252
edx-opaque-keys==0.4
53-
#edx-drf-extensions==1.2.2
53+
# edx-organizations 0.4.4 requires edx-drf-extensions<1.0.0,>=0.5.1, but you'll have edx-drf-extensions 1.2.2 which is incompatible.
54+
edx-drf-extensions==1.2.2
5455
edx-organizations==0.4.4
5556

5657

5758
##
5859
## Devsite
5960
##
6061

61-
django-debug-toolbar==1.11
62+
django-debug-toolbar==1.9.1
6263

6364

6465
##
@@ -69,10 +70,13 @@ coverage==4.5.1
6970
factory-boy==2.5.1
7071
pylint==1.8.2
7172
pylint-django==0.9.1
72-
pytest==3.1.3
73+
pytest==3.6.2
7374
pytest-django==3.1.2
7475
pytest-mock==1.7.1
7576
pytest-pythonpath==0.7.2
7677
pytest-cov==2.6.0
7778
tox==3.7.0
7879
freezegun==0.3.12
80+
81+
# Added to address: TypeError: attrib() got an unexpected keyword argument 'convert'
82+
attrs==19.1.0

devsite/requirements/hawthorn_community.txt

+14-5
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ recommonmark==0.4.0
4848
##
4949

5050
edx-opaque-keys==0.4.4
51-
#edx-drf-extensions==1.5.2
51+
edx-drf-extensions==1.5.2
5252

5353
edx-organizations==0.4.10
5454

@@ -66,13 +66,22 @@ django-debug-toolbar==1.11
6666
coverage==4.5.4
6767
factory-boy==2.5.1
6868
flake8==3.7.9
69-
pylint==1.9.5
70-
pylint-django==0.11.1
71-
pytest==3.1.3
69+
# To address: edx-lint 0.5.5 requires pylint==1.7.1, but you'll have pylint 1.9.5 which is incompatible.
70+
pylint==1.7.1
71+
# To address edx-lint 0.5.5 requires pylint-django==0.7.2, but you'll have pylint-django 0.11.1 which is incompatible.
72+
pylint-django==0.7.2
73+
pytest==3.6.2
7274
pytest-django==3.1.2
7375
pytest-mock==1.7.1
7476
pytest-pythonpath==0.7.2
7577
pytest-cov==2.6.0
76-
tox==3.14.2
78+
# To address: tox 3.14.2 requires pluggy<1,>=0.12.0, but you'll have pluggy 0.6.0 which is incompatible.
79+
tox==3.1.0
7780
freezegun==0.3.12
7881
edx-lint==0.5.5
82+
83+
# Added to address: TypeError: attrib() got an unexpected keyword argument 'convert'
84+
attrs==19.1.0
85+
86+
# To address: edx-lint 0.5.5 requires astroid==1.5.2, but you'll have astroid 1.6.6 which is incompatible.
87+
astroid==1.5.2

devsite/requirements/hawthorn_multisite.txt

+7-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ recommonmark==0.4.0
4949
##
5050

5151
edx-opaque-keys==0.4.4
52-
#edx-drf-extensions==1.5.2
52+
edx-drf-extensions==1.5.2
5353

5454
#edx-organizations==0.4.10
5555
# * Organization/site mapping requires Appsembler's fork
@@ -72,10 +72,14 @@ factory-boy==2.5.1
7272
flake8==3.7.9
7373
pylint==1.9.5
7474
pylint-django==0.11.1
75-
pytest==3.1.3
75+
pytest==3.6.2
7676
pytest-django==3.1.2
7777
pytest-mock==1.7.1
7878
pytest-pythonpath==0.7.2
7979
pytest-cov==2.6.0
80-
tox==3.14.2
80+
# tox==3.14.5
81+
tox==3.1.0
8182
freezegun==0.3.12
83+
84+
# Added to address: TypeError: attrib() got an unexpected keyword argument 'convert'
85+
attrs==19.1.0

figures/helpers.py

+68-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
1-
'''
2-
Helper functions to make data handling and conversions easier
3-
'''
1+
"""Helper functions to make data handling and conversions easier
2+
3+
# Figures 0.3.13 - Defining scope of this module
4+
5+
The purpose of this module is to provide conveniece methods around commonly
6+
executed statements. These convenience methods should serve as shorthand for
7+
code we would repeatedly execute which don't yet have a module of their own.
8+
9+
The idea is that if there is not yet a module for some helper function, we put
10+
it in here until we see a pattern. We then either identify a new module and
11+
transplate these like functions out of this module into the new one. Or we
12+
identify that functions in here actually should go in an existing module.
13+
14+
The purposes for this are:
15+
16+
1. Reduce development time cost by not having to stop and design (informally or
17+
formally) a new module or inspect all the existing modules to find an
18+
appropriate home. The second point is helpful for those not intimately
19+
familiar with the Figures codebase
20+
21+
2. Avoid premature optimization in building out new module functionality because
22+
we are adding a single method (Avoid the desire to fill this new module with
23+
more than the new functionality that serves our immediate needs)
24+
25+
3. Avoid over specificifity, which can results in an explosion of tiny modules
26+
that may be too specific in their context
27+
28+
29+
## Background
30+
31+
Originally this module served as a variant of a 'utils' module, but with the
32+
express purpose of providing convenience "helper" functions to help DRY (do not
33+
repeat yourself) the code and make the code more readable
34+
35+
## What does not belong here?
36+
37+
* "Feature" functionality does not belong here
38+
* Long functions do not belong here
39+
* Code that communicates outside of Figures does not belong here. No database,
40+
filesystem, or network connectiviely functionality belongs here
41+
42+
This is not an exhaustive list. We'll grow it as needed.
43+
44+
An important point is that we should not expect this module to be a permanent
45+
home for functionality.
46+
"""
447

548
import calendar
649
import datetime
@@ -19,6 +62,8 @@ def is_multisite():
1962
A naive but reliable check on whether Open edX is using multi-site setup or not.
2063
2164
Override by setting ``FIGURES_IS_MULTISITE`` to true in the Open edX FEATURES.
65+
66+
TODO: Move to `figures.sites`
2267
"""
2368
return bool(settings.FEATURES.get('FIGURES_IS_MULTISITE', False))
2469

@@ -28,19 +73,23 @@ def log_pipeline_errors_to_db():
2873
Capture pipeline errors to the figures.models.PipelineError model.
2974
3075
Override by setting ``FIGURES_LOG_PIPELINE_ERRORS_TO_DB`` to false in the Open edX FEATURES.
76+
77+
TODO: This is an environment/setting "getter". Should be moved to `figures.settings`
3178
"""
3279
return bool(settings.FEATURES.get('FIGURES_LOG_PIPELINE_ERRORS_TO_DB', True))
3380

3481

3582
def as_course_key(course_id):
36-
'''Returns course id as a CourseKey instance
83+
"""Returns course id as a CourseKey instance
3784
3885
Convenience method to return the paramater unchanged if it is of type
3986
``CourseKey`` or attempts to convert to ``CourseKey`` if of type str or
4087
unicode.
4188
4289
Raises TypeError if an unsupported type is provided
43-
'''
90+
91+
NOTE: This is a good example of a helper method that belongs here
92+
"""
4493
if isinstance(course_id, CourseKey):
4594
return course_id
4695
elif isinstance(course_id, basestring): # noqa: F821
@@ -54,6 +103,11 @@ def as_datetime(val):
54103
'''
55104
TODO: Add arg flag to say if caller wants end of day, beginning of day
56105
or a particular time of day if the param is a datetime.date obj
106+
107+
108+
NOTE: The date functions here could be in a `figures.datetools` module.
109+
110+
Not set on the name `datetools` but some date specific module
57111
'''
58112
if isinstance(val, datetime.datetime):
59113
return val
@@ -74,10 +128,15 @@ def as_datetime(val):
74128

75129

76130
def as_date(val):
77-
'''Casts the value to a ``datetime.date`` object if possible
131+
"""Casts the value to a ``datetime.date`` object if possible
78132
79133
Else raises ``TypeError``
80-
'''
134+
135+
NOTE: This is a good example of a helper method that belongs here
136+
We could also move this and the other date helpers to a "date"
137+
labeled module in Figures. Then at some future time, move those out
138+
into a "toolbox" package to abstrac
139+
"""
81140
# Important to check if datetime first because datetime.date objects
82141
# pass the isinstance(obj, datetime.date) test
83142
if isinstance(val, datetime.datetime):
@@ -117,13 +176,11 @@ def days_in_month(month_for):
117176

118177
# TODO: Consider changing name to 'months_back_iterator' or similar
119178
def previous_months_iterator(month_for, months_back):
120-
'''Iterator returns a year,month tuple for n months including the month_for
179+
"""Iterator returns a year,month tuple for n months including the month_for
121180
122181
month_for is either a date, datetime, or tuple with year and month
123182
months back is the number of months to iterate
124-
125-
includes the month_for
126-
'''
183+
"""
127184

128185
if isinstance(month_for, tuple):
129186
# TODO make sure we've got just two values in the tuple

figures/log.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Provides logging and instrumentation functionality for Figures
2+
3+
"""
4+
5+
from contextlib import contextmanager
6+
import logging
7+
import timeit
8+
9+
10+
default_logger = logging.getLogger(__name__)
11+
12+
13+
@contextmanager
14+
def log_exec_time(description, logger=None):
15+
"""Context handler to log execution time info for a block
16+
17+
Parameters:
18+
description : The text to add to the log statement
19+
logger : The logger to receive the log statement
20+
21+
If `logger' is not provided, then the default logger is used,
22+
23+
`logging.getLogger(__name__)`
24+
25+
Example:
26+
27+
```
28+
with log_exec_time('Collect grades for courses in site',logger=my_logger):
29+
do_grades_collection(site=my_site)
30+
```
31+
"""
32+
logger = logger if logger else default_logger
33+
start_time = timeit.default_timer()
34+
yield
35+
elapsed = timeit.default_timer() - start_time
36+
msg = '{}: {} s'.format(description, elapsed)
37+
38+
logger.info(msg)

figures/models.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class LearnerCourseGradeMetricsManager(models.Manager):
191191
def most_recent_for_learner_course(self, user, course_id):
192192
queryset = self.filter(user=user, course_id=str(course_id))
193193
if queryset:
194-
return queryset.order_by('-date_for')[0] # pylint: disable=E1101
194+
return queryset.order_by('-date_for')[0]
195195
else:
196196
return None
197197

@@ -234,7 +234,7 @@ def completed_for_site(self, site, **_kwargs):
234234
# We do the string casting in case couse_ids are CourseKey instance
235235
filter_args['course_id__in'] = [str(key) for key in course_ids]
236236
if filter_args:
237-
qs = qs.filter(**filter_args) # pylint: disable=E1101
237+
qs = qs.filter(**filter_args)
238238
return qs
239239

240240
def completed_ids_for_site(self, site, **_kwargs):
@@ -400,7 +400,7 @@ def latest_for_site_month(self, site, year, month):
400400
If no record found, returns 'None'
401401
"""
402402
queryset = self.filter(site=site, date_for__year=year, date_for__month=month)
403-
return queryset.order_by('-modified').first() # pylint: disable=no-member
403+
return queryset.order_by('-modified').first()
404404

405405

406406
@python_2_unicode_compatible
@@ -450,7 +450,7 @@ def latest_for_course_month(self, site, course_id, year, month):
450450
date_for__year=year,
451451
date_for__month=month,
452452
)
453-
return queryset.order_by('-modified').first() # pylint: disable=no-member
453+
return queryset.order_by('-modified').first()
454454

455455

456456
@python_2_unicode_compatible

figures/pipeline/enrollment_metrics.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def collect_metrics_for_enrollment(site, course_enrollment, course_sm, date_for=
166166
lcgm = LearnerCourseGradeMetrics.objects.filter(
167167
user=course_enrollment.user,
168168
course_id=str(course_enrollment.course_id))
169-
most_recent_lcgm = lcgm.order_by('date_for').last() # pylint: disable=E1101
169+
most_recent_lcgm = lcgm.order_by('date_for').last()
170170

171171
if _enrollment_metrics_needs_update(most_recent_lcgm, most_recent_sm):
172172
progress_data = _collect_progress_data(most_recent_sm)

figures/settings/lms_production.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def update_celerybeat_schedule(celerybeat_schedule_settings, figures_env_tokens)
4949
),
5050
}
5151

52-
if figures_env_tokens.get('ENABLE_FIGURES_MONTHLY_METRICS', False):
52+
if figures_env_tokens.get('ENABLE_FIGURES_MONTHLY_METRICS', True):
5353
celerybeat_schedule_settings['figures-monthly-metrics'] = {
5454
'task': 'figures.tasks.run_figures_monthly_metrics',
5555
'schedule': crontab(0, 0, day_of_month=1),

figures/tasks.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from student.models import CourseEnrollment # pylint: disable=import-error
1717

1818
from figures.helpers import as_course_key, as_date
19+
from figures.log import log_exec_time
1920
from figures.models import PipelineError
2021
from figures.pipeline.course_daily_metrics import CourseDailyMetricsLoader
2122
from figures.pipeline.site_daily_metrics import SiteDailyMetricsLoader
@@ -232,6 +233,8 @@ def populate_mau_metrics_for_site(site_id, month_for=None, force_update=False):
232233
TODO: Check results of 'store_mau_metrics' to log unexpected results
233234
"""
234235
site = Site.objects.get(id=site_id)
236+
msg = 'Starting figures'
237+
logger.info(msg)
235238
for course_key in figures.sites.get_course_keys_for_site(site):
236239
populate_course_mau(site_id=site_id,
237240
course_id=str(course_key),
@@ -253,8 +256,11 @@ def populate_all_mau():
253256

254257
@shared_task
255258
def populate_monthly_metrics_for_site(site_id):
259+
256260
site = Site.objects.get(id=site_id)
257-
fill_last_smm_month(site=site)
261+
msg = 'Ran populate_monthly_metrics_for_site. [{}]:{}'
262+
with log_exec_time(msg.format(site.id, site.domain)):
263+
fill_last_smm_month(site=site)
258264

259265

260266
@shared_task

0 commit comments

Comments
 (0)