Finished checking level_about

Finished checking level_about

--- server/materials/data-en/level_about.coffee
+++ server/materials/data-en/level_about.coffee
@@ -353,7 +353,7 @@

I assume that you will go through the sublevels sequentially, moving to a new sublevel usually when you have "passed" the previous sublevel. Exception: if you have passed some topic in sublevel A or B, and you see a contest of advanced tasks on the same topic in sublevel C or D, then you can solve it.

-
The requirements for the "passage" of each sublevel are indicated under its heading. As a rule, for sublevels A and B it is required to solve all the tasks, for levels C and D it is required to solve part (half or a third) of the tasks. However, this does not mean that the remaining tasks of levels C and D do not need to be solved. I recommend that if you have solved the necessary minimum of tasks of levels B and D, go to the next level in order to quickly begin studying a new theory. But at the same time, return regularly to the unfinished tasks of the previous levels. It can be estimated approximately as follows: when you move to level 4A, you should have solved half of the tasks of level 3C, 3/4 of the tasks of level 2C and about 7/8 of the tasks of levels 1C and 1D. Similarly, when you move to level 5A, you should have solved half of the tasks of level 4C, as well as 3/4 of the tasks of level 3C, 7/8 of the tasks of level 2C and almost all (with the rare exception of particularly unpleasant tasks) tasks of levels 1C and 1D. Similarly for other levels.

+
The requirements for the "passing" of each sublevel are indicated under its heading. As a rule, for sublevels A and B it is required to solve all the tasks, for levels C and D it is required to solve part (half or a third) of the tasks. However, this does not mean that the remaining tasks of levels C and D do not need to be solved. I recommend that if you have solved the necessary minimum of tasks of levels C and D, go to the next level in order to quickly begin studying a new theory. But at the same time, return regularly to the unfinished tasks of the previous levels. It can be estimated approximately as follows: when you move to level 4A, you should have solved half of the tasks of level 3C, 3/4 of the tasks of level 2C and about 7/8 of the tasks of levels 1C and 1D. Similarly, when you move to level 5A, you should have solved half of the tasks of level 4C, as well as 3/4 of the tasks of level 3C, 7/8 of the tasks of level 2C and almost all (with the rare exception of particularly unpleasant tasks) tasks of levels 1C and 1D. Similarly for other levels.

Please note that in levels C and D, the tasks are not always ordered by complexity. Therefore, solve them in the order in which it is more convenient for you!

@@ -366,7 +366,7 @@

If you are already teaching someone (schoolchildren, students, etc.) programming, and you want to use the algoprog or its materials in your classes...

-
...or if you want to start teaching someone on the basis of the algprog or using its materials...

+
...or if you want to start teaching someone on the algprog or using its materials...

... or if you want to use the algoprog engine in your classes by filling it with your own materials

@@ -374,28 +374,26 @@

Materials

-
If you do not need the functionality of the algprog, but only materials — theoretical materials, video lectures, tasks, etc. — then they are freely available in one form or another. Theoretical materials, the author of which I am, as a rule, are distributed under one or another free license, you can use them. It is usually indicated in the material itself, if the license is not specified there, then ask me. The materials that I am not the author of are taken from other sites, you can usually use them too. Almost all tasks are taken from public sites informatics.mccme.ru and codeforces.com on both of these sites, you can form your courses from the tasks available there, etc. (in particular, computer science is specially made for conducting various courses). Basic (although already outdated) the contents of the algoprog are on informatics: informatics.msk.ru/course/view.php?id=1135 — actually, the whole algoprog grew out of my computer science course.

+
If you do not need the functionality of the algprog, but only materials — theoretical materials, video lectures, tasks, etc. — then they are freely available in one form or another. Theoretical materials, the author of which I am, as a rule, are distributed under one or another free license, you can use them. It is usually indicated in the material itself, if the license is not specified there, then ask me. The materials that I am not the author of are taken from other sites, you can usually use them too. Almost all tasks are taken from public sites informatics.msk.ru and codeforces.com, on both of these sites you can form your courses from the tasks available there, etc. Basic (although already outdated) contents of the algoprog are on informatics: informatics.msk.ru/course/view.php?id=1135 — actually, the whole algoprog grew out of my informatics course.

Engine

-
The algoprog engine itself is also freely available, you can raise your own instance of algoprog and do anything there. (Although, of course, it's not that easy, and I'm unlikely to help you.)

+
The algoprog engine itself is also freely available, you can set up your own instance of algoprog and do anything there. (Although, of course, it's not that easy, and I'm unlikely to help you.)

A separate instance

-
I can raise a separate instance of the algoprog for your students, with your materials and tasks. This requires a separate discussion, but in principle it is possible. It will be paid, the specific cost will depend on the necessary work to support the instance, the necessary improvements, and the expected number of students. As a rough preliminary assessment, if no serious improvements are required, and 20-50 students are expected, then the cost of a separate instance will be about 1000 rubles per student per month; if there are fewer students, then the cost, of course, will be higher.

+
I can set up a separate instance of the algoprog for your students, with your materials and tasks. This requires a separate discussion, but in principle it is possible. The cost will depend on the necessary work to support the instance, the necessary improvements, and the expected number of students. As a rough preliminary assessment, if no serious improvements are required, and 20-50 students are expected, then the cost of a separate instance will be about 1000 rubles per student per month; if there are fewer students, then the cost, of course, will be higher.

Classes directly on the algoprog based on the materials of the algoprog

-
If you want to do it on the algoprog, then there are the following options. Firstly, your students can register on the algoproge and study as independent students. From my point of view, these will be just additional students on the algoproge, you will be able to organize classes, help students, etc., but you will not have any additional opportunities on the algoproge (you will not be able to view student decisions, etc.). Your students are engaged independently, for them the cost of classes will, of course, be determined by the general rules, of course, I will not take money from you (and in general I may not know about you :)).

-
-
If you want to have additional opportunities — to view students' decisions, comment on them, count/ignore them, have separate summary tables, then write to me. All of this (as well as other requests from your side) is quite realistic, but requires a separate discussion. As basic options , there are the following:

-

-
Your students are engaged in the algoproge on an equal footing with the rest, I check them, comment on them, count/ignore their decisions — and plus you can do the same; there will be separate signs for your students, etc.As a rule, the cost of such classes for your students will be the same as if they were engaged independently. (That is, there is no additional payment for additional functionality, but there are no discounts either.)

-
I don't interact with your students in any way — I don't look at their decisions, I don't count/ignore them. You do all this, if you want.As a rule, the cost of such classes for your students will be two times less than if they were engaged independently.

+
If you want to do it on the algoprog, then there are the following options. Firstly, your students can register on the algoproge and study as independent students. From my point of view, these will be just additional students on the algoprog, you will be able to organize classes, help students, etc., but you will not have any additional rights on the algoprog itself (you will not be able to view student submissions, etc.). Your students are engaged independently, for them the cost of classes will, of course, be determined by the general rules, of course, I will not take money from you (and in general I may not know about you at all :)).

+
+
If you want to have additional features — to view students' submissions, comment on them, accept/ignore them, have separate summary tables, then contact me. All of this (as well as other requests from your side) is quite realistic, but requires a separate discussion. As basic options , there are the following:

+

+
Your students are studying in the algoprog on an equal footing with the rest, I check their submissions, comment on them, accept/ignore — and plus you can do the same; there will be separate rankings for your students, etc.As a rule, the cost of such classes for your students will be the same as if they were studying independently. (That is, there is no additional payment for additional functionality, but there are no discounts either.)

+
I don't interact with your students in any way — I don't look at their code, I don't accept/ignore them. You do all this, if you want.As a rule, the cost of such classes for your students will be two times less than if they were engaged independently.

In both variants above, the words "basic" (variants) and "as a rule" are essential. This means that, on the one hand, this is not a public offer — in a particular situation, other conditions and other prices are possible; on the other hand, other options are also possible in terms of functionality and in our interaction with you. Write, let's talk :)

-
-
In particular, if algoprog classes would be free for your students if they were engaged in algoprog themselves, then both options above will be, as a rule, free.

""")

@@ -403,49 +401,43 @@
     page(
         " \nAbout achievements",
                               String.raw"""

About achievements

-
On the algoprog, each user can earn achievements. Achievements are given out for different things, first of all for successful classes on the algoprog, as well as for participation in various contests. The achievements earned by the user are shown in his profile, also the three most "cool" achievements are shown next to the user's name in summary tables, etc.

+
On the algoprog, each user can earn achievements. Achievements are given out for different things, first of all for successful study on the algoprog itself, as well as for participation in various programming contests. The achievements earned by the user are shown in his profile, also the three most "cool" achievements are shown next to the user's name in rankings, etc.

The full list of available achievements can be viewed here, as well as in the form of source code here. In any place (including the link in the previous sentence), you can click on the achievement and see a list of all users who have this achievement.

-
Some of the achievements are issued automatically, some of the achievements (mainly for the Olympics) I install manually. If you think that you should have some kind of achievement, but you don't have it, then write to me. This is especially true for schoolchildren from outside the Nizhny Novgorod region - I can't always keep track of all your contests, so write to me about your contest results yourself.

""")
+
Some of the achievements are issued automatically, some of the achievements (mainly for the contests) I assign manually. If you think that you should have some achievement, but you don't have it, then write to me. This is especially true for schoolchildren from outside the Nizhny Novgorod region - I can't always keep track of all your contests, so write to me about your contest results yourself.

""")

 tshirts = () ->
     page(
         "About T-shirts",
                              String.raw"""
About T-shirts

Free T-shirts

-
For achieving certain levels , T - shirts are issued according to the following scheme:

+
For achieving certain levels, T - shirts are issued according to the following scheme:

Reaching Level 3C — violet T-shirt "binary search"

-
Reaching Level 5C — blue T-shirt "intersection of straight lines"

+
Reaching Level 5C — blue T-shirt "intersection of lines"

Reaching Level 7C — orange T-shirt "components of strong connectivity"

Reaching Level 9C — red T-shirt "cartesian tree"

Reaching Level 11C — black T-shirt "matching"

-
T-shirts are given free of charge to all students studying at the algoprog. I am ready to give out T-shirts to Nizhny Novgorod residents in class, I am ready to send them to non-Nizhny Novgorod residents in Russia
+
T-shirts are given free of charge to all students studying at the algoprog. I am ready to send them to non-Nizhny Novgorod residents in Russia
         by a transport company (CDEK or Boxberry) at my expense (with the exception of hard-to-reach regions where neither CDEK nor Boxberry are transported);
-        if you need to be sent to a remote region or outside of Russia, then the shipment is at your expense.

-
In order for me to give you a T-shirt, write in advance so that I bring it to class. T-shirts come in different sizes, men's and women's.
-        Write the size and cut. T-shirts for level 3B are "in stock", most likely, I will just bring the T-shirt you need to the next lesson,
-        or I will send it within a couple of days. T-shirts for higher levels are made to order, within a few weeks.

-
T-shirts are also issued to those who were engaged in the algoprog before, but are not engaged now (including those who used to be engaged for a fee,
-        but who doesn't have an account paid for now — if you've been studying for a T-shirt before, I'll be happy to give it to you for free).
-        Those who were engaged in the pre-reform algoprog
-        (with the old level system), as well as those who were engaged before the introduction of the level system, in principle, I am also ready to give out T-shirts,
-        but the correspondence of the current (above) levels and those levels / those that were before, I will set each time individually.
-

+        if you want me to sent a tshirt to a remote region or outside of Russia, then the shipment is at your expense.

+
In order for me to give you a T-shirt, contact me. T-shirts come in different sizes, men's and women's.
+        Write the size and style. T-shirts for level 3B are "in stock", most likely, I will just send the T-shirt you need within a few days.
+        T-shirts for higher levels are made to order, within a few weeks.

         """, {id: "tshirts"})

 module20927_29 = () ->
     page(
         "\nFormulas for calculating rating, activity, etc.",
-                                                              String.raw"""

Rating calculation formulas, etc.

-
The summary tables show the rating, activity, rating on codeforces, weighted rating change on codeforces and weighted number of contests on codeforces.

+                                                              String.raw"""

Rating formulas, etc.

+
The rankings tables show the rating, activity, rating on codeforces, weighted rating change on codeforces and weighted number of contests on codeforces.

Rating

(Who cares, the specific code for calculating the rating and activity is here.)

-
The rating is determined simply by a set of solved problems. Namely, each task costs a certain fixed number of rating points, determined by the level of this task.

+
The rating is determined simply by a set of solved problems. Namely, each problem costs a certain fixed number of rating points, determined by the level of this problem.

A Level 1A task costs $2.5$ rating points. A Level 2A task costs $2.5^2$ rating points. A level 3A task costs $2.5^3$ rating points, etc. A level $N$A task costs $2.5^N$ rating points.

@@ -455,8 +447,8 @@

When displaying the rating in tables, it is rounded to integers, but when calculating everything is considered real numbers.

-
Cost plate (rounded to two decimal places):

-
          A B C D
+
Cost table (rounded to two decimal places):

+
          A            B        C         D
          1       2.50       3.14       3.95       4.97
          2       6.25       7.86       9.88
          3      15.62      19.65      24.71      31.07
@@ -473,23 +465,23 @@

Ideology

-
The scores for the task grow very quickly, especially so that at high levels it is not so important that you solve at lower levels. For example, if you are already at level 5, then most likely, level 1 tasks do not pose any difficulty at all for you. Therefore, it does not matter how much you have solved at level 1. And this is due to the fact that level 5 tasks cost much more than level 1 tasks.

+
The scores for the task grow very quickly, because at high levels it is not so important what you have solved at lower levels. For example, if you are already at level 5, then most likely, level 1 problems do not pose any difficulty at all for you. Therefore, it does not matter how much you have solved at level 1. And this is due to the fact that level 5 problems cost much more than level 1 problems.

Activity

-
(The specific calculation code is at the same link)

-
-
Activity is an indicator of how actively you have been solving problems lately. Every task that you have ever solved gives its contribution to activity. Each task has "basic" activity scores — this is the square root of the level number. (All level 1 tasks have a "base" score of 1, each level 2 task has a base score of 1.4, etc.) This base score is multiplied by $0.55^{t/\tau}$, where $t$ is the time elapsed since you passed this task, and $\strut\tau$ is equal to 1 week. For example, if you have just passed the task, then the base score is taken into account in full. If you passed the task exactly a day ago, then the base score is multiplied by $0.55^{1/7}\approx 0.92$. If you passed the task exactly two weeks ago, then the base score is multiplied by $0.55^2\approx.3$. If you passed the task 8 weeks ago, then the base score is multiplied by $0.55^8\approx.0083$, i.e. this task is almost not taken into account.

-
-
The total activity value for all tasks is divided by 0.45, resulting in a sort of average number of "tasks" per week.

-
-
It turns out that in fact only recently completed tasks are taken into account, and those that you have solved for a long time are not taken into account. But at the same time, the decline in this "accountability" is smooth. If you stop solving problems, then your activity will gradually decrease over time (although it will never become exactly zero). In the tables, activity is displayed rounded to one decimal place, but in fact, activity is a real real number.

-
-
In a number of summary tables, students whose activity is greater than 0.1 are indicated first, and only then all the others.

+
(The calculation code is at the same link)

+
+
Activity is an indicator of how actively you have been solving problems lately. Every problem that you have ever solved gives its contribution to activity. Each problem has "basic" activity score — this is the square root of the level number. (All level 1 problems have a "base" score of 1, each level 2 problem has a base score of 1.4, etc.) This base score is multiplied by $0.55^{t/\tau}$, where $t$ is the time elapsed since you solved this problems, and $\strut\tau$ is equal to 1 week. For example, if you have just solved the problem, then the base score is taken into account in full. If you solved the problem exactly a day ago, then the base score is multiplied by $0.55^{1/7}\approx 0.92$. If you solved the problem exactly two weeks ago, then the base score is multiplied by $0.55^2\approx.3$. If you solved the problem 8 weeks ago, then the base score is multiplied by $0.55^8\approx.0083$, i.e. this problem is almost not taken into account.

+
+
The total activity value for all problems is divided by 0.45, resulting in a sort of average number of "problems" per week.

+
+
As a result, only recently solved problems are taken into account, and those that you have solved a long time ago are not taken into account. But at the same time, the decline in this "accountability" is smooth. If you stop solving problems, then your activity will gradually decrease over time (although it will never become exactly zero). In the rankings, activity is displayed rounded to one decimal place, but in fact, activity is a real real number.

+
+
In some ranking tables, students whose activity is greater than 0.1 are indicated first, and only then all the others.

Colour

-
The names of schoolchildren in the tables are drawn in a color determined by their rating and activity. Namely, the activity determines the brightness of the color (from almost black to saturated), and the rating determines the shade (from purple through all the colors of the rainbow to red). For example, a student with a rating of 1 and activity of 10 will have a bright purple color, a student with a rating of 1 and activity of 0.1 will have a dark purple color, a student with a rating of about 100,000 and activity of 10 will have a bright green color, and with activity of 0.1 — dark green color; red color corresponds to a rating of 4500000.

-
-
Specific formula: In the HSV model, the participant's color has the following components:

+
The names of students in the tables are drawn in a color determined by their rating and activity. Namely, the activity determines the brightness of the color (from almost black to saturated), and the rating determines the shade (from purple through all the colors of the rainbow to red). For example, a student with a rating of 1 and activity of 10 will have a bright purple color, a student with a rating of 1 and activity of 0.1 will have a dark purple color, a student with a rating of about 100,000 and activity of 10 will have a bright green color, and with activity of 0.1 — dark green color; red color corresponds to a rating of 4500000.

+
+
Specific formula: In the HSV model, the participant's color has the following components:

         $h = k\cdot \log(r+r_0) + b$

         $v = 0.3 + 0.7 \cdot \log(a+1) / \log(A + 1)$

         $s = 1$

@@ -497,7 +489,7 @@

There are also minor corrections at the edges of possible value intervals. The code is here.

-
codeforces parameters

+
Codeforces parameters

Code

The CF rating is taken directly from CF, updated once a day.

@@ -510,22 +502,21 @@
     page(
         "About the \"Find a bug\" section",
                                           String.raw"""
About the "Find a bug" section

-
When you have passed the solution to the problem and received an "Accepted" for it, you have the opportunity to look for bugs in the decisions of other students on this problem. Namely, a link "Find an bug" appears on the task page, which opens a list of specially selected incorrect solutions to this problem.
+
When you have solved a problem and received an "Accepted" for it, you have the opportunity to look for bugs in the submits of other students on this problem. Namely, a link "Find an bug" appears on the task page, which opens a list of specially selected incorrect solutions to this problem.
         (Of course, the link does not appear in every task, but only in those tasks where there are such specially selected solutions.)
-        There is also a section in the site menu "Find the bug", where there are links to all solutions in which you can look for errors
+        There is also a section in the site menu "Find a bug", where there are links to all solutions in which you can look for errors
         (not all of these solutions are available to you — solutions are available to you only for those tasks for which you received a "Accepted").

-
About each of the solutions in which you need to find an error, it is known that the error can be corrected by making no more than 15 edits in the solution code. An edit is considered to be adding, deleting, or changing one character (i.e., the Levenshnein distance is actually considered). Your task is to find the error, fix it, and get OK on the task. Even with a margin, you are given the opportunity to make up to 23 edits.

-
-
In the error search interface, you are immediately given a text editor in which the solution code is open. I strongly recommend looking for errors directly in this editor, without copying the code anywhere to yourself (to your IDE or editor), and accordingly not using a debugger, etc., but only looking at the code with your eyes. But in the most difficult cases, you can copy the code to yourself and understand it more thoroughly.

+
For each of the solutions in which you need to find an error, it is known that the error can be corrected by making no more than 15 edits in the solution code. An edit is considered to be adding, deleting, or changing one character (i.e., the Levenshnein distance is actually considered). Your task is to find the error, fix it, and get OK on the task. To give you some margin, you are allowed to make up to 23 edits.

+
+
In the "Find a bug" interface, you are immediately given a text editor in which the solution code is open. I strongly recommend looking for errors directly in this editor, without copying the code anywhere (like your IDE or editor), and accordingly not using a debugger, etc., but only looking at the code with your eyes. But in the most difficult cases, you can copy the code and understand it more thoroughly.

The solutions in the "Find a bug" section are available in different programming languages. Don't be afraid to look for bugs even in languages you don't know. I try not to allow those solutions in which the error is caused by language problems to search for errors, almost always errors have more algorithmic reasons, and, accordingly, you can find them, even if you do not know the features of the language.

-
I do not look at the solutions sent through the "find a bug" section with my eyes and do not comment (in particular, it is impossible to get "accepted" for them), and they do not affect your results in the general table. Of course, if you can't find an error, you can write to me, I will advise you something.

+
I do not look at the solutions sent through the "find a bug" section with my eyes and do not comment (in particular, it is impossible to get "accepted" for them), and they do not affect your results in the rankings. Of course, if you can't find an error, you can write to me, I will advise you something.

Each solution, in addition to the problem, is also characterized by an individual number (a combination of four letters or numbers). Use this number to distinguish between solutions for finding errors for the same problem; in particular, if you want to ask me about such a solution, then specify both the name of the problem and the number of the solution (well, or just send a link to the error search page in this particular solution).

-
-
So far, in principle, there are not very many solutions in which to look for errors; over time, I think there will be more of them.
""", {id: "about_find_mistake"})
+        """, {id: "about_find_mistake"})

 contacts = () ->
     label("""
@@ -539,12 +530,12 @@

Telegram: @pkalinin

Telegram channel with important announcements: @algoprog_news (mainly in Russian).

Telegram chat for all algoprog students: Algoprog-chat.
-        To join, you should first specify your telegram id or username in your algoprog profile.

+        To join, you should first specify your telegram id in your algoprog profile.

My contact details can be freely distributed for any questions related to the course or
         with olympiad programming in general.

If you have any problems with working in the course, any questions, etc., write to me immediately!

-
* VKontakte friend policy: I don't mind if you add me as a friend,
+
* VK friend policy: I don't mind if you add me as a friend,
         but I will only add those who have been successfully engaged for a long time.

** Please call by phone only in emergency cases. If your question is not very urgent and can
         wait a few hours, write to me in any way indicated above — in VKontakte, in Telegram or by email.
@@ -576,7 +567,7 @@
         module20927_7(),
         module20927_9(),
         label(
-            "
About how to study here
"),
+            "
How to study here
"),
         work_with_site(),
         module20927_13(),
         module20927_15(),
@@ -587,7 +578,7 @@
         label(
             "
Pay for classes
"),
         label(
-            "
And a little more...
"),
+            "
And a bit more...
"),
         module20927_23(),
         module20927_25(),
         module20927_29(),


Use coffeescriptisms instead of javascript

Minor change to use 'is' instead of '==' and other similar changes.

--- src/rules/indentation.coffee
+++ src/rules/indentation.coffee
@@ -31,7 +31,7 @@

     # Return an error if the given indentation token is not correct.
     lintToken: (token, tokenApi) ->
-        [type, numIndents, lineNumber] = token
+        [type, numIndents, { first_line: lineNumber }] = token

         if type in [ "[", "]" ]
             @lintArray(token)
@@ -44,7 +44,7 @@
         # so ignore such cases. Are there other times an indentation
         # could possibly follow a '+'?
         previous = tokenApi.peek(-2)
-        isInterpIndent = previous and previous[0] == '+'
+        isInterpIndent = previous and previous[0] is '+'

         # Ignore the indentation inside of an array, so that
         # we can allow things like:
@@ -84,13 +84,11 @@
             numIndents = currentLine.match(/^(\s*)/)[1].length
             numIndents -= previousIndentation

-
         # Now check the indentation.
         expected = tokenApi.config[@rule.name].value
-        if not ignoreIndent and numIndents != expected
-            return {
-                context: "Expected #{expected} got #{numIndents}"
-            }
+        if not ignoreIndent and numIndents isnt expected
+            return { context: "Expected #{expected} got #{numIndents}" }
+
     # Return true if the current token is inside of an array.
     inArray : () ->
         return @arrayTokens.length > 0
@@ -98,9 +96,9 @@
     # Lint the given array token.
     lintArray : (token) ->
         # Track the array token pairs
-        if token[0] == '['
+        if token[0] is '['
             @arrayTokens.push(token)
-        else if token[0] == ']'
+        else if token[0] is ']'
             @arrayTokens.pop()
         # Return null, since we're not really linting
         # anything here.


adding mtime

adding mtime

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -166,7 +166,7 @@
   ###
   window.Sprite.loadByName = (name, callback) ->
     directory = App?.directories?.images || "images"
-    url = "#{BASE_URL}/#{directory}/#{name}.png#{MTIME}"
+    url = "#{BASE_URL}/#{directory}/#{name}.png?#{MTIME}"
     Sprite.load(url, callback)

   window.Sprite.create = Sprite


support exact schedule matches

support exact schedule matches

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -462,8 +462,20 @@
       query: q
     }
     pagerDutyGet msg, "/schedules", query, (json) ->
+      schedule = null
+      # Single result returned
       if json.schedules and json.schedules.length == 1
-        cb(json.schedules[0])
+        schedule = json.schedules[0]
+
+      # Multiple results returned and one is exact
+      if json.schedules and json.schedules.length > 1
+        matchingExactly = json.schedules.filter (s) ->
+          s.name == q
+        if matchingExactly.length == 1
+          schedule = matchingExactly[0]
+
+      if schedule
+        cb(schedule)
       else
         # maybe look for a specific name match here?
         msg.send "#{q} matched #{json.schedules.length} schedules. Can you be more specific?"


Improve graphs in message history

Improve graphs in message history

--- lib/message-history-element.coffee
+++ lib/message-history-element.coffee
@@ -5,11 +5,26 @@
   initialize: (messageManager, @editor) ->
     messageManager.onDidReceiveMessage (message) => @handleMessage(message)
     messageManager.onGraphCreationRequest (request) => @handleGraphCreationRequest(request)
+    messageManager.onGraphDrawingRequest (request) => @handleGraphDrawingRequest(request)

   getTitle: ->
     @editor.getTitle() + ' Csound output'

   handleGraphCreationRequest: ({name, data}) ->
+    graphElement = document.createElement 'div'
+    @divsForGraphIDs ?= {}
+    @divsForGraphIDs[data.windid] = graphElement
+
+    @handleGraphDrawingRequest data
+
+    nextSibling = @nextSiblingsForGraphCaptions?[data.caption.trim()]
+    if nextSibling
+      @insertBefore graphElement, nextSibling
+    else
+      @appendChild graphElement
+    @scrollTop += graphElement.clientHeight
+
+  handleGraphDrawingRequest: (data) ->
     margin = {top: 20, right: 20, bottom: 30, left: 50}
     width = 650 - margin.left - margin.right
     height = 340 - margin.top - margin.bottom
@@ -55,13 +70,12 @@
         .attr('class', 'line')
         .attr('d', line)

-    @appendChild(document.createElement 'div').innerHTML = svgElement.outerHTML
+    graphElement = @divsForGraphIDs?[data.windid]
+    graphElement?.innerHTML = svgElement.outerHTML

   handleMessage: ({string, attributes}) ->
     if @lastChild?.localName isnt 'pre'
       @messageContainer = @appendChild document.createElement 'pre'
-
-    needsScroll = (@scrollTop >= @scrollHeight - @clientHeight)

     messageType = attributes & csound.CSOUNDMSG_TYPE_MASK
     if messageType is csound.CSOUNDMSG_DEFAULT
@@ -119,7 +133,11 @@
         span.classList.add className
     span.appendChild document.createTextNode string

-    if needsScroll
-      @scrollTop = @scrollHeight - @clientHeight
+    @scrollTop = @scrollHeight - @clientHeight
+
+    if /^ftable\s*\d+/.test string
+      @messageContainer = @appendChild document.createElement 'pre'
+      @nextSiblingsForGraphCaptions ?= {}
+      @nextSiblingsForGraphCaptions[string.trim()] = @messageContainer

 module.exports = MessageHistoryElement = document.registerElement 'csound-message-history', prototype: MessageHistoryElement.prototype


webapp: in files, create+upload button wrap on narrow screens. does not look ok.

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -1968,7 +1968,7 @@
             actions      = {@props.actions} />

     render_new_file : ->
-        <Col sm=3>
+        <Col sm=3 style={whiteSpace: 'nowrap'}>
             <ProjectFilesNew
                 file_search   = {@props.file_search}
                 current_path  = {@props.current_path}


Correct function reference

Correct function reference

`getByUpstreamName` -> `this.getByUpstreamName`

--- BaragonUI/app/models/Service.coffee
+++ BaragonUI/app/models/Service.coffee
@@ -89,7 +89,7 @@
                         loadBalancerGroups: @attributes.service.loadBalancerGroups
                         options: @attributes.options
                     addUpstreams: []
-                    removeUpstreams: [getByUpsteamName(upstream, requestId)]
+                    removeUpstreams: [@getByUpsteamName(upstream, requestId)]
                     noValidate: noValidate,
                     noReload: noReload
                 }


based on watching people -- change project creation some; I was sooo wrong about the autoscroll being a good idea.

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -747,10 +747,6 @@
         if e.keyCode == 13 and @state.title_text != ''
             @create_project()

-    go_to_upgrade: (e) ->
-        e.preventDefault();
-        $('html, body').animate({ scrollTop: $('#upgrade_before_creation').offset().top }, 0)
-
     render_upgrade_before_create: (subs) ->
         <Col sm=12>

@@ -773,32 +769,26 @@
             @setState(create_button_hit: 'with_members_and_network')
         else
             @setState(create_button_hit: 'with_custom_upgrades')
-            @scroll_to_billing()
-
-    scroll_to_billing: ->
-        setTimeout ( ->
-            $('#smc-react-container > div').scrollTop($("#new_project_billing_section").offset().top - 30)
-        ), 500

     render_upgrade_buttons: ->
         <ButtonToolbar>
-            Create this project with:

+            <Button
+                disabled  = {@state.title_text == '' or @state.state == 'saving'}
+                onClick   = {=>@create_project(false)}
+                bsStyle  = 'success' >
+                Create project
+            </Button>
             <Button
                 disabled = {@state.title_text == '' or @state.state == 'saving' or @state.create_button_hit == 'with_members_and_network'}
                 bsStyle  = 'success'
                 onClick  = {=>@create_project_with_members_and_network()} >
-                <Icon name="arrow-circle-up" /> Hosting and network upgrades…
+                <Icon name="arrow-circle-up" /> Create with hosting and network upgrades…
             </Button>
             <Button
                 disabled = {@state.title_text == '' or @state.state == 'saving' or @state.create_button_hit == 'with_custom_upgrades'}
                 bsStyle  = 'success'
-                onClick  = {=>@setState(create_button_hit: 'with_custom_upgrades');@scroll_to_billing()} >
-                <Icon name="cog" /> Custom upgrades...
-            </Button>
-            <Button
-                disabled  = {@state.title_text == '' or @state.state == 'saving'}
-                onClick   = {=>@create_project(false)} >
-                No upgrades
+                onClick  = {=>@setState(create_button_hit: 'with_custom_upgrades')} >
+                <Icon name="cog" /> Create with custom upgrades...
             </Button>
             <Button
                 disabled = {@state.state is 'saving'}
@@ -850,7 +840,7 @@
                     Create
                 </Button>
                 <Button
-                    onClick  = {=>@setState(create_button_hit: '');$('#smc-react-container > div').scrollTop($("#new_project_title").offset().top - 30)} >
+                    onClick  = {=>@setState(create_button_hit: '')} >
                     Cancel
                 </Button>
             </ButtonToolbar>


webapp: concatinate path for links and images in sagews using "join". fixes e.g. %md [test.jpg](test.jpg) when in a sagews in $HOME

--- src/smc-webapp/sagews.coffee
+++ src/smc-webapp/sagews.coffee
@@ -1298,6 +1298,7 @@
                     # internal link
                     y.click (e) ->
                         target = $(@).attr('href')
+                        {join} = require('path')
                         if target.indexOf('/projects/') == 0
                             # fully absolute (but without https://...)
                             target = decodeURI(target.slice('/projects/'.length))
@@ -1306,10 +1307,10 @@
                             target = decodeURI(target.slice(1))  # just get rid of leading slash
                         else if target[0] == '/'
                             # absolute inside of project
-                            target = "#{that.project_id}/files#{decodeURI(target)}"
+                            target = join(that.project_id, 'files', decodeURI(target))
                         else
                             # relative to current path
-                            target = "#{that.project_id}/files/#{that.file_path()}/#{decodeURI(target)}"
+                            target = join(that.project_id, 'files', that.file_path(), decodeURI(target))
                         redux.getActions('projects').load_target(target, not(e.which==2 or (e.ctrlKey or e.metaKey)))
                         return false

@@ -1341,7 +1342,8 @@
             file_path = @file_path()
             if misc.startswith(src, '/')
                 file_path = ".smc/root/#{file_path}"
-            new_src = "#{window.smc_base_url}/#{@project_id}/raw/#{file_path}/#{src}"
+            {join} = require('path')
+            new_src = join(window.smc_base_url, @project_id, 'raw', file_path, src)
             y.attr('src', new_src)

     _post_save_success: () =>


Nuke commented code

Nuke commented code

--- src/snippet-expansion.coffee
+++ src/snippet-expansion.coffee
@@ -69,7 +69,5 @@

   restore: (@editSession) ->
     @editSession.snippetExpansion = this
-#     @tabStopMarkers = @tabStopMarkers.map (anchorRange) =>
-#       @editSession.addAnchorRange(anchorRange.getBufferRange())

 _.extend(SnippetExpansion.prototype, Subscriber)


indent check

indent check

--- packages/rocketchat-ui/lib/chatMessages.coffee
+++ packages/rocketchat-ui/lib/chatMessages.coffee
@@ -187,17 +187,17 @@
 								Meteor.call 'slashCommand', {cmd: command, params: param, msg: msgObject }
 							return

-					if !RocketChat.settings.get('Message_AllowUnrecognizedSlashCommand')
-						invalidCommandMsg =
-							_id: Random.id()
-							rid: rid
-							ts: new Date
-							msg: TAPi18n.__('No_such_command', { command: match[1] })
-							u:
-								username: "rocketbot"
-							private: true
-						ChatMessage.upsert { _id: invalidCommandMsg._id }, invalidCommandMsg
-						return
+						if !RocketChat.settings.get('Message_AllowUnrecognizedSlashCommand')
+							invalidCommandMsg =
+								_id: Random.id()
+								rid: rid
+								ts: new Date
+								msg: TAPi18n.__('No_such_command', { command: match[1] })
+								u:
+									username: "rocketbot"
+								private: true
+							ChatMessage.upsert { _id: invalidCommandMsg._id }, invalidCommandMsg
+							return

 				Meteor.call 'sendMessage', msgObject
 				done()


add resize event utilities

add resize event utilities

--- src/coffee/breadcrumbsEatr.coffee
+++ src/coffee/breadcrumbsEatr.coffee
@@ -424,6 +424,15 @@

             return null

+        o.resizeStart = ->
+            o.opts.onResizeStart(_this) if $.isFunction(o.opts.onResizeStart)
+
+        o.resizeUpdate = ->
+            o.opts.onResizeUpdate(_this) if $.isFunction(o.opts.onResizeUpdate)
+
+        o.resizeComplete = ->
+            o.opts.onResizeComplete(_this) if $.isFunction(o.opts.onResizeComplete)
+
         o.isParentsHidden = ->
             o.hiddenParents = if typeof o.hiddenParents is "undefined" then o.el.parentsUntil('body', ':hidden') else o.hiddenParents.filter(':hidden')


Update router to empty the contents of main on the first render

--- src/coffee/cilantro/router.coffee
+++ src/coffee/cilantro/router.coffee
@@ -15,6 +15,14 @@
             @_loaded = []
             @_routes = {}
             @_handlers = {}
+
+        _empty: =>
+            if not @_emptied
+                @_emptied = true
+                # This should be simply $(...).empty(), however, IE8 throws
+                # experiences an exception, so accessing the children then
+                # removing them works..
+                Backbone.$(@options.main).children().remove()

         _unloadAll: =>
             @_unload(@_registered[id], false) for id in @_loaded.slice()
@@ -54,6 +62,7 @@
             if not view._rendered
                 view._rendered = true
                 if options.el isnt false
+                    @_empty()
                     if options.el?
                         target = Backbone.$(options.el, @options.main)
                     else


home: default to metrics

home: default to metrics

--- src/pages/home/index.coffee
+++ src/pages/home/index.coffee
@@ -29,7 +29,7 @@
   render: =>
     {selectedIndex} = @state.getValue()

-    tabs = ['experiments', 'metrics']
+    tabs = ['metrics', 'experiments']

     z '.p-home',
       z @$menu,


Fixed import statement to use proper capitalization

--- SingularityUI/app/components/common/atomicDisplayItems/Link.cjsx
+++ SingularityUI/app/components/common/atomicDisplayItems/Link.cjsx
@@ -1,6 +1,6 @@
 React = require 'react'
 OverlayTrigger = require 'react-bootstrap/lib/OverlayTrigger'
-ToolTip = require 'react-bootstrap/lib/ToolTip'
+ToolTip = require 'react-bootstrap/lib/Tooltip'

 Link = React.createClass


Update current task title after ticket loading failed.

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -133,6 +133,9 @@
             status: status
             detail: detail
           )
+
+        # update current task title
+        App.Event.trigger 'task:render'
     )


fixed little error

fixed little error

--- app/assets/javascripts/autocomplete_factura_compra.js.coffee
+++ app/assets/javascripts/autocomplete_factura_compra.js.coffee
@@ -12,10 +12,10 @@
       window.Helpers.AutocompleteCompraHelper.total_producto($this)
       window.Helpers.AutocompleteCompraHelper.suma_productos_factura_compra()

-    # $(".nombre_producto").on "input", ->
-    #   $this = $(this)
-    #   texto = $this.val()
-    #   $this.closest(".fields").find(".nombre_item").text(texto + " $")
+    $(".nombre_producto").blur "input", ->
+      $this = $(this)
+      texto = $this.val()
+      $this.closest(".fields").find(".nombre_item").text(texto + " $")

     $(".eliminar_item_producto").on "click", ->
       $this = $(this)
@@ -36,7 +36,7 @@
         $this = $(this)
         # $this.closest(".fields").find(".producto_id").val ui.item.id
         $(".producto_id").val ui.item.id
-        $this.closest(".fields").find(".nombre_item").text($this.val() + " $")
+        # $this.closest(".fields").find(".nombre_item").text($this.val() + " $")
         $this.closest(".fields").find(".nombre_generico").val ui.item.nombre_generico
         $this.closest(".fields").find(".codigo").val ui.item.codigo
         $this.closest(".fields").find(".categoria").val ui.item.categoria


Add some logs

Add some logs

--- src/coffee/sync/sync.coffee
+++ src/coffee/sync/sync.coffee
@@ -20,12 +20,14 @@
         logConfig:
           logger: @_logger

+    @_logger.debug "New #{@constructor.name} object"
     @_data = {}
     @_utils = new Utils

   buildActions: (new_obj, old_obj) ->
     # diff 'em
     diff = @_utils.diff(old_obj, new_obj)
+    @_logger.debug diff, "JSON diff for #{@constructor.name} object"
     update = undefined
     if diff
       actions = @_doMapActions(diff, new_obj, old_obj)
@@ -36,6 +38,7 @@
     @_data =
       update: update
       updateId: old_obj.id
+    @_logger.debug @_data, "Data update for #{@constructor.name} object"
     this

   get: (key = 'update') -> @_data[key]


avoid errors when enter is pressed shortly after `

--- client/keymap.coffee
+++ client/keymap.coffee
@@ -83,7 +83,7 @@
 ks = '`1234567890-=qwertyuiop[]asdfghjk l;\'\\zxcvbnm,./~!@#$%^&*()_+QWERTYUIOP{}ASDFGHJKL:"|ZXCVBNM<>?'.split /\s*/
 vs = '`¨¯<≤=≥>≠∨∧×÷?⍵∊⍴~↑↓⍳○*←→⍺⌈⌊_∇∆∘\'⎕⍎⍕ ⊢ ⊂⊃∩∪⊥⊤|⍝⍀⌿⋄⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⌹?⍵⍷⍴⍨↑↓⍸⍥⍣⍞⍬⍺⌈⌊_∇∆⍤⌸⌷≡≢⊣⊂⊃∩∪⊥⊤|⍪⍙⍠'.split /\s*/
 bqc = []
-CodeMirror.keyMap.dyalogBackquote = nofallthrough: true, disableInput: true
+CodeMirror.keyMap.dyalogBackquote = fallthrough: 'dyalog', disableInput: true
 if ks.length != vs.length then console.error? 'bad configuration of backquote keymap'
 ks.forEach (k, i) ->
   v = vs[i]; reverse[v] ?= k


kbd locales in alphabetical order

kbd locales in alphabetical order

--- client/keymap.coffee
+++ client/keymap.coffee
@@ -119,28 +119,6 @@
 # They can be indexed by scancode: http://www.abreojosensamblador.net/Productos/AOE/html/Pags_en/ApF.html
 # "APL" and "APL shifted" are the defaults upon which the user can build customisations.
 layoutDesc =
-  US: '''
-    ☠ ` 1 2 3 4 5 6 7 8 9 0 - = ☠ ☠   ☠ ~ ! @ # $ % ^ & * ( ) _ + ☠ ☠
-    ☠ q w e r t y u i o p [ ] \\      ☠ Q W E R T Y U I O P { } |
-    ☠ a s d f g h j k l ; ' ☠ ☠       ☠ A S D F G H J K L : " ☠ ☠
-    ☠ ☠ z x c v b n m , . / ☠ ☠       ☠ ☠ Z X C V B N M < > ? ☠ ☠
-
-    ☠ ⋄ ¨ ¯ < ≤ = ≥ > ≠ ∨ ∧ × ÷ ☠ ☠   ☠ ¤ ⌶ ⍫ ⍒ ⍋ ⌽ ⍉ ⊖ ⍟ ⍱ ⍲ ! ⌹ ☠ ☠
-    ☠ ? ⍵ ∊ ⍴ ~ ↑ ↓ ⍳ ○ * ← → ⊢       ☠ ? ⍵ ⍷ ⍴ ⍨ ↑ ↓ ⍸ ⍥ ⍣ ⍞ ⍬ ⊣
-    ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ∘ ' ⎕ ⍎ ⍕ ☠ ☠       ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ⍤ ⌸ ⌷ ≡ ≢ ☠ ☠
-    ☠ ☠ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍝ ⍀ ⌿ ☠ ☠       ☠ ☠ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍪ ⍙ ⍠ ☠ ☠
-  '''
-  UK: '''
-    ☠ ` 1 2 3 4 5 6 7 8 9 0 - = ☠ ☠   ☠ ¬ ! " £ $ % ^ & * ( ) _ + ☠ ☠
-    ☠ q w e r t y u i o p [ ] ☠       ☠ Q W E R T Y U I O P { } ☠
-    ☠ a s d f g h j k l ; ' # ☠       ☠ A S D F G H J K L : @ ~ ☠
-    ☠ \\z x c v b n m , . / ☠ ☠       ☠ | Z X C V B N M < > ? ☠ ☠
-
-    ☠ ⋄ ¨ ¯ < ≤ = ≥ > ≠ ∨ ∧ × ÷ ☠ ☠   ☠ ¤ ⌶ ⍫ ⍒ ⍋ ⌽ ⍉ ⊖ ⍟ ⍱ ⍲ ! ⌹ ☠ ☠
-    ☠ ? ⍵ ∊ ⍴ ~ ↑ ↓ ⍳ ○ * ← → ☠       ☠ ? ⍵ ⍷ ⍴ ⍨ ↑ ↓ ⍸ ⍥ ⍣ ⍞ ⍬ ☠
-    ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ∘ ' ⎕ ⍎ ⍕ ⊢ ☠       ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ⍤ ⌸ ⌷ ≡ ≢ ⊣ ☠
-    ☠ ⊢ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍝ ⍀ ⌿ ☠ ☠       ☠ ⊣ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍪ ⍙ ⍠ ☠ ☠
-  '''
   DK: '''
     ☠ ½ 1 2 3 4 5 6 7 8 9 0 + ´ ☠ ☠   ☠ § ! " # ¤ % & / ( ) = ? ` ☠ ☠
     ☠ q w e r t y u i o p å ¨ ☠       ☠ Q W E R T Y U I O P Å ^ ☠
@@ -162,6 +140,28 @@
     ☠ ? ⍵ ∊ ⍴ ~ ↑ ↓ ⍳ ○ * ← → ☠       ☠ ? ⍵ ⍷ ⍴ ⍨ ↑ ↓ ⍸ ⍥ ⍣ ⍞ ⍬ ☠
     ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ∘ ' ⎕ ⍎ ⍕ ⊢ ☠       ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ⍤ ⌸ ⌷ ≡ ≢ ⊣ ☠
     ☠ ⊢ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍝ ⍀ ⌿ ☠ ☠       ☠ ⊣ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍪ ⍙ ⍠ ☠ ☠
+  '''
+  UK: '''
+    ☠ ` 1 2 3 4 5 6 7 8 9 0 - = ☠ ☠   ☠ ¬ ! " £ $ % ^ & * ( ) _ + ☠ ☠
+    ☠ q w e r t y u i o p [ ] ☠       ☠ Q W E R T Y U I O P { } ☠
+    ☠ a s d f g h j k l ; ' # ☠       ☠ A S D F G H J K L : @ ~ ☠
+    ☠ \\z x c v b n m , . / ☠ ☠       ☠ | Z X C V B N M < > ? ☠ ☠
+
+    ☠ ⋄ ¨ ¯ < ≤ = ≥ > ≠ ∨ ∧ × ÷ ☠ ☠   ☠ ¤ ⌶ ⍫ ⍒ ⍋ ⌽ ⍉ ⊖ ⍟ ⍱ ⍲ ! ⌹ ☠ ☠
+    ☠ ? ⍵ ∊ ⍴ ~ ↑ ↓ ⍳ ○ * ← → ☠       ☠ ? ⍵ ⍷ ⍴ ⍨ ↑ ↓ ⍸ ⍥ ⍣ ⍞ ⍬ ☠
+    ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ∘ ' ⎕ ⍎ ⍕ ⊢ ☠       ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ⍤ ⌸ ⌷ ≡ ≢ ⊣ ☠
+    ☠ ⊢ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍝ ⍀ ⌿ ☠ ☠       ☠ ⊣ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍪ ⍙ ⍠ ☠ ☠
+  '''
+  US: '''
+    ☠ ` 1 2 3 4 5 6 7 8 9 0 - = ☠ ☠   ☠ ~ ! @ # $ % ^ & * ( ) _ + ☠ ☠
+    ☠ q w e r t y u i o p [ ] \\      ☠ Q W E R T Y U I O P { } |
+    ☠ a s d f g h j k l ; ' ☠ ☠       ☠ A S D F G H J K L : " ☠ ☠
+    ☠ ☠ z x c v b n m , . / ☠ ☠       ☠ ☠ Z X C V B N M < > ? ☠ ☠
+
+    ☠ ⋄ ¨ ¯ < ≤ = ≥ > ≠ ∨ ∧ × ÷ ☠ ☠   ☠ ¤ ⌶ ⍫ ⍒ ⍋ ⌽ ⍉ ⊖ ⍟ ⍱ ⍲ ! ⌹ ☠ ☠
+    ☠ ? ⍵ ∊ ⍴ ~ ↑ ↓ ⍳ ○ * ← → ⊢       ☠ ? ⍵ ⍷ ⍴ ⍨ ↑ ↓ ⍸ ⍥ ⍣ ⍞ ⍬ ⊣
+    ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ∘ ' ⎕ ⍎ ⍕ ☠ ☠       ☠ ⍺ ⌈ ⌊ _ ∇ ∆ ⍤ ⌸ ⌷ ≡ ≢ ☠ ☠
+    ☠ ☠ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍝ ⍀ ⌿ ☠ ☠       ☠ ☠ ⊂ ⊃ ∩ ∪ ⊥ ⊤ | ⍪ ⍙ ⍠ ☠ ☠
   '''
 @layouts = layouts = {}
 do ->


this removes hack from #974 as firefox version is now well beyond when bug was fixed

--- src/components/tutor-popover.cjsx
+++ src/components/tutor-popover.cjsx
@@ -57,14 +57,6 @@

     @setState(imagesLoading: currentImageStatus)

-  setMaxImageWidth: (image, maxWidth) ->
-    # unfortunately, css max-width does not work in firefox inside display: table elements
-    # https://bugzilla.mozilla.org/show_bug.cgi?id=975632
-    # See https://github.com/openstax/tutor-js/issues/977 for full details.
-    return unless image?
-    {width} = image.getBoundingClientRect?()
-    image.width = maxWidth if width > maxWidth
-
   areImagesLoading: ->
     _.compact(@state.imagesLoading).length isnt 0

@@ -78,7 +70,6 @@
     viewer = @refs.popover.getDOMNode()
     images = @getImages()
     {height, width} = viewer.getBoundingClientRect()
-    _.each(images, _.partial(@setMaxImageWidth, _, width - 30)) if width > 0

     scrollable = false


Opera does require a single leading new line apparently

--- src/trans-eventsource.coffee
+++ src/trans-eventsource.coffee
@@ -14,10 +14,10 @@

 exports.app =
     eventsource: (req, res) ->
-        res.setHeader('Content-type', 'text/event-stream; charset=UTF-8')
+        res.setHeader('Content-Type', 'text/event-stream; charset=UTF-8')
         res.writeHead(200)
-        # opera needs to hear two more initial new lines.
-        res.write(['', ''].join('\r\n'))
+        # Opera needs one more new line at the start.
+        res.write('\r\n')

         session = transport.Session.bySessionIdOrNew(req.session, req.sockjs_server)
         session.register( new EventSourceReceiver(res, req.sockjs_server.options) )


fixed wrong x,x return error

fixed wrong x,x return error

Former-commit-id: 6587663be9b93c9dddafb9762ce28321bc4f0523 [formerly 6828f1b880f8652d0db3fb45fd0abf84c0216c1b]
Former-commit-id: 68f552a608f7cae82aeeb4f5f33119f85190f06d

--- js/msa/stage/tileStage.coffee
+++ js/msa/stage/tileStage.coffee
@@ -78,9 +78,13 @@
       mouseX = e.offsetX
       mouseY = e.offsetY

-      unless e.offsetX?
+      unless mouseX?
         mouseX = e.layerX
         mouseY = e.layerY
+
+      unless mouseX?
+        mouseX = e.pageX
+        mouseY = e.pageY

       unless mouseX?
         console.log e
@@ -88,7 +92,7 @@
         return

       # TODO: else
-      return [mouseX,mouseX]
+      return [mouseX,mouseY]

     _onDblClick: (e) ->
       #if not @rect? or @rect?.left is 0


[wip] 全TextNodeのスペース以外の文字を〼に置き換えるようになった

[wip] 全TextNodeのスペース以外の文字を〼に置き換えるようになった

styleタグとscriptタグの中身も書き換えてしまうので機能追加が必要

--- src/w-cart.coffee
+++ src/w-cart.coffee
@@ -23,8 +23,9 @@

     console.log 'aaa'

-    # capture @, url, 'original.png'
+    capture @, url, '0-before.png'
     result = @evaluate replaceAllChars
+    capture @, url, '1-after.png'

     console.log result
     console.log 'bbb'
@@ -48,6 +49,15 @@

 ###
   開いたページのすべての文字列を〼に置き換える関数
+  DOMをbody要素から再帰的にチェックしてTextNodeだったら書き換える
 ###
 replaceAllChars = ->
-  return document.title
+  replaceTextNode = (node)->
+    if node.nodeType is Node.TEXT_NODE
+      node.nodeValue = node.nodeValue.replace /\S/g, '〼'
+      return null
+    else
+      for child in node.childNodes
+        replaceTextNode child
+
+  replaceTextNode document.body


allow effective-datatables:initialize to be targetted

--- app/assets/javascripts/effective_datatables/initialize.js.coffee
+++ app/assets/javascripts/effective_datatables/initialize.js.coffee
@@ -1,5 +1,5 @@
-initializeDataTables = ->
-  $('table.effective-datatable:not(.initialized)').each ->
+initializeDataTables = (target) ->
+  $(target || document).find('table.effective-datatable:not(.initialized)').each ->
     datatable = $(this)
     options = datatable.data('options') || {}
     buttons_export_columns = options['buttons_export_columns'] || ':not(.col-actions)'
@@ -181,6 +181,8 @@
   $('table.effective-datatable').each -> try $(this).removeClass('initialized').DataTable().destroy()

 $ -> initializeDataTables()
+$(document).on 'effective-datatables:initialize', (event) -> initializeDataTables(event.currentTarget)
+
 $(document).on 'page:change', -> initializeDataTables()
 $(document).on 'turbolinks:load', -> initializeDataTables()
 $(document).on 'turbolinks:render', -> initializeDataTables()


added some model extensions.

added some model extensions.

--- app/assets/javascripts/habberdash/lib/spine_extensions.js.coffee
+++ app/assets/javascripts/habberdash/lib/spine_extensions.js.coffee
@@ -1,13 +1,13 @@
+# Controller Additions
+#----------------------------------------------------------------------------------------------------/
 Spine.Controller.include
   view: (name, options = {}) ->
     JST["habberdash/views/#{name}"](options)
-

   trace: ->
     return unless App.debugMode && window.console && window.console.log
     try console.log(arguments)
     catch e
-

   delegateEvents: (events) ->
     @releaseEvents ||= []
@@ -27,7 +27,6 @@
       else
         @el.delegate(selector, eventName, method)

-
   release: (callback) ->
     for event in @releaseEvents || []
       $(event.element)[event.unbindMethod](event.eventName, event.method)
@@ -35,3 +34,39 @@
       @bind 'release', callback
     else
       @trigger 'release'
+
+
+# Model Additions
+#----------------------------------------------------------------------------------------------------/
+makeArray = (args) ->
+  Array::slice.call(args, 0)
+
+class Habberdash.Model extends Spine.Model
+  @options = (attributes...) ->
+    @optionAttributes = attributes if attributes.length
+    @optionAttributes and= makeArray(@optionAttributes)
+    @optionAttributes or= []
+    this
+
+  @configure = (name, attributes...) ->
+    @className  = name
+    @records    = {}
+    @crecords   = {}
+    @attributes = attributes if attributes.length
+    @attributes and= makeArray(@attributes)
+    @attributes or= []
+    @attributes.push(optionAttribute) for optionAttribute in (@optionAttributes || [])
+    @unbind()
+    this
+
+  options: ->
+    result = {}
+    return result unless @constructor.optionAttributes
+    for key in @constructor.optionAttributes when key of this
+      if typeof @[key] is 'function'
+        result[key] = @[key]()
+      else
+        result[key] = @[key]
+    result.id = @id if @id
+    result
+


The angularjs controller method that returns trusted HTML for entry contents works better in the cases in which the entry does not have either a summary or a content attribute.

--- app/assets/javascripts/ng-controllers/ng-FeedbunchCtrl.js.coffee
+++ app/assets/javascripts/ng-controllers/ng-FeedbunchCtrl.js.coffee
@@ -205,9 +205,9 @@
   $scope.trustedEntryContent = (entry)->
     html = ''
     # Return the content if present; otherwise try to return the summary.
-    if entry.content.length > 0
+    if entry.content?.length > 0
       html = entry.content
-    else if entry.summary.length > 0
+    else if entry.summary?.length > 0
       html = entry.summary
     return $sce.trustAsHtml html


Remove unnecessary logging now that public photos and videos are working.

--- src/ui/views/messages.coffee
+++ src/ui/views/messages.coffee
@@ -480,11 +480,9 @@
     embed_item = att?[0]?.embed_item
     {plus_photo, data, type_} = embed_item ? {}
     if plus_photo?
-        console.error('plus_photo', plus_photo)
         href  = plus_photo.data?.thumbnail?.image_url
         thumb = href.replace /googleusercontent.com\/(.+)\/s0\/(.+)$/, 'googleusercontent.com/$1/s512/$2'
         original_content_url = plus_photo.data?.original_content_url
-        console.error(href, thumb)
         isVideo = plus_photo.data?.media_type isnt 'MEDIA_TYPE_PHOTO'
         if isVideo
             if plus_photo.videoinformation?


minor improvements in machine list

minor improvements in machine list

* also listen to `hubot show machines`
* put mac address in ticks to format it as code

--- src/wakeup.coffee
+++ src/wakeup.coffee
@@ -5,10 +5,10 @@
 #   None
 #
 # Commands:
-#   hubot kick <machine> - Send a WOL package to <machine>
-#   hubot machine <name> has mac <mac> - store <mac> for machine <name>
+#   hubot kick <machine> - Send a WOL package to `<machine>`
+#   hubot machine <name> has mac <mac> - store `<mac>` for machine `<name>`
 #   hubot forget machine <name> - remove a machine from memory
-#   hubot show me machine list - returns a list of machines with their mac adres
+#   hubot show machines - returns a list of machines with their mac adres
 #
 # Notes:
 #   None
@@ -55,15 +55,15 @@
   #
   # Show a list of all machines I know
   #
-  robot.respond /show (?:the )?machine list/i, (msg) ->
+  robot.respond /show (?:the )?machine list|show machines/i, (msg) ->
     machineList = robot.brain.get('wakeup') or null

-    machineReply = null
+    machineReply = ""

     if machineList == null
       msg.reply "I don't know any machines... (hint: hubot machine <name> has mac <mac>)"
     else
-      machineReply = "#{machineReply}\n #{mac} | #{machine}" for machine, mac of machineList
+      machineReply = "#{machineReply}\n `#{mac}` | #{machine}" for machine, mac of machineList

       msg.reply machineReply


Change optional require format to make webpack happier

--- src/yayson.coffee
+++ src/yayson.coffee
@@ -1,18 +1,11 @@
-
-tryRequire = (dep) ->
-  try
-    require dep
-  catch
-    undefined
-
 @window ||= {}

 Q = @window.Q
 _ = @window._

-Q ||= tryRequire 'q'
-_ ||= tryRequire 'lodash/dist/lodash.underscore'
-_ ||= tryRequire 'underscore'
+Q ||= (try require? 'q')
+_ ||= (try require? 'lodash/dist/lodash.underscore')
+_ ||= (try require? 'underscore')

 utils = require('./yayson/utils')(_, Q)

@@ -32,3 +25,5 @@
   Presenter: presenter({adapter})
   Adapter: Adapter

+
+


[improvement] Added the ability to pass a model to CollectionEditor#edit (3m)

--- vendor/assets/javascripts/neat/collection_editor.coffee
+++ vendor/assets/javascripts/neat/collection_editor.coffee
@@ -215,6 +215,8 @@
     _.indexOf @views, @viewInEdit

   edit: (view)->
+    if view instanceof Backbone.Model
+      view = @__findViewForModel(view)
     if view
       @viewInEdit?.save()
       view.edit()
@@ -236,6 +238,10 @@

+  __findViewForModel: (model)->
+    _.find @views, (view)-> view.model.cid is model.cid
+
+
   debug: (o...)->
     @log(o...) if Neat.debug


activity: defensive control structures for `JGroup#description` reference usages

--- client/Social/Activity/ContentDisplays/ActivityRightBlock.coffee
+++ client/Social/Activity/ContentDisplays/ActivityRightBlock.coffee
@@ -103,7 +103,9 @@

       @innerContaner = new KDCustomHTMLView cssClass : "right-block-box"

-      hasBody = group.body.trim() isnt ""
+      {body}  = group
+      body   ?= ""
+      hasBody = Boolean body.trim().length
       isAdmin = "admin" in KD.config.roles

       edit = new CustomLinkView
@@ -121,7 +123,7 @@

       @bodyView = new KDCustomHTMLView
         tagName   : "p"
-        pistachio : "{{ #(body)}}"
+        pistachio : "{{ #(body) or ''}}"
         cssClass  : "group-description"
       , group


landing: teams: minor style fix

landing: teams: minor style fix

--- client/landing/site.landing/coffee/teams/teamselectorform.coffee
+++ client/landing/site.landing/coffee/teams/teamselectorform.coffee
@@ -1,7 +1,7 @@
-kd      = require 'kd'
-utils   = require './../core/utils'
-JView   = require './../core/jview'
-
+kd             = require 'kd'
+utils          = require './../core/utils'
+JView          = require './../core/jview'
+LoginInputView = require './../login/logininputview'

 module.exports = class TeamsSelectorForm extends kd.FormView

@@ -16,17 +16,16 @@
     teams    = utils.getPreviousTeams()
     lastTeam = teams?.latest

-    @inputView = new kd.CustomHTMLView
-      cssClass     : 'login-input-view'
-      click        : => @input.setFocus()
+    @teamName = new LoginInputView
+      inputOptions   :
+        label        : 'Team URL'
+        placeholder  : 'your-team-name'
+        name         : 'slug'
+        defaultValue : lastTeam  if lastTeam

-    @inputView.addSubView @input = new kd.InputView
-      placeholder  : 'your-team-name'
-      name         : 'slug'
-      defaultValue : lastTeam  if lastTeam
-
-    @inputView.addSubView @suffix = new kd.View
+    @suffix = new kd.View
       tagName      : 'span'
+      cssClass     : 'TeamDomainSuffix'
       partial      : ".#{kd.config.domains.main}"

@@ -37,13 +36,14 @@
       attributes  : { testpath : 'goto-team-button' }
       type        : 'submit'

-    @input.on 'ValidationFeedbackCleared', =>
-      @inputView.unsetClass 'validation-error validation-passed'
+    @teamName.input.on 'ValidationFeedbackCleared', =>
+      @teamName.input.unsetClass 'validation-error validation-passed'

   pistachio: ->

     """
-    {{> @inputView}}
-
{{> @button}}

+    {{> @teamName }}
+    {{> @suffix }}
+
{{> @button }}

     """


client: pass params to callback KodingRouter#handleRoute

--- client/app/lib/kodingrouter.coffee
+++ client/app/lib/kodingrouter.coffee
@@ -121,7 +121,7 @@
       method += 'WithOptions'
       options = {model:models, route, query}

-    callback = =>
+    callback = ({ name, section, models, route, query, passOptions, options, method }) =>
       kd.getSingleton("appManager").tell section, method, options ? models, (contentDisplay) =>
         unless contentDisplay
           console.warn 'no content display'
@@ -139,9 +139,15 @@
       groupName = if section is "Groups" then name else "koding"
       groupsController.changeGroup groupName, (err) =>
         showError err if err
-        callback()
+        callback {
+          name, section, models, route,
+          query, passOptions, options, method
+        }
     else
-      callback()
+      callback {
+        name, section, models, route,
+        query, passOptions, options, method
+      }

   loadContent: (name, section, slug, route, query, passOptions) ->


LO has the book id prefixed

LO has the book id prefixed

--- src/components/tags/lo.cjsx
+++ src/components/tags/lo.cjsx
@@ -1,28 +1,86 @@
 React = require 'react'
+_ = require 'underscore'
+classnames = require 'classnames'

-MultiInput = require './multi-input'
+Error = require './error'
+Wrapper = require './wrapper'
+{ExerciseActions, ExerciseStore} = require '../../stores/exercise'

-LoTags = React.createClass
+PREFIX = 'lo'
+BookSelection = require './book-selection'
+
+Input = React.createClass
+
+  getDefaultProps: ->
+    inputType: 'text'

   propTypes:
-    exerciseId: React.PropTypes.string.isRequired
+    tag: React.PropTypes.string.isRequired
+
+  getInitialState: ->
+    [book, lo] = @props.tag.split(':')
+    {book, lo}

   validateInput: (value) ->
     'Must match LO pattern of dd-dd-dd' unless value.match(
       /^\d{1,2}-\d{1,2}-\d{1,2}$/
     )

-  cleanInput: (val) ->
-    val.replace(/[^0-9\-]/g, '')
+  componentWillReceiveProps: (nextProps) ->
+    [book, lo] = @props.tag.split(':')
+    @setState({book, lo})
+
+  onTextChange: (ev) ->
+    lo = ev.target.value.replace(/[^0-9\-]/g, '')
+    @setState({errorMsg: null, lo})
+
+  validateAndSave: (attrs = {}) ->
+    {lo, book} = _.defaults attrs, @state
+    if book and lo?.match( /^\d{1,2}-\d{1,2}-\d{1,2}$/ )
+      ExerciseActions.setPrefixedTag(@props.exerciseId,
+        prefix: PREFIX, tag: "#{book}:#{lo}", previous: @props.tag
+      )
+    else
+      @setState({lo, book, errorMsg: 'Must match LO pattern of book:dd-dd-dd'})
+
+  onTextBlur: -> @validateAndSave()
+  updateBook: (ev) -> @validateAndSave(book: ev.target.value)
+
+  onDelete: ->
+    ExerciseActions.setPrefixedTag(@props.exerciseId,
+      prefix: PREFIX, tag: false, previous: @props.tag
+    )

   render: ->
-    <MultiInput
-      {...@props}
-      label='LO'
-      prefix='lo'
-      placeholder='nn-nn-nn'
-      cleanInput={@cleanInput}
-      validateInput={@validateInput}
-    />

-module.exports = LoTags
+

+      <BookSelection onChange={@updateBook} selected={@state.book} />
+      <input
+        className='form-control'
+        type={@props.inputType}
+        onChange={@onTextChange}
+        onBlur={@onTextBlur}
+        value={@state.lo}
+        placeholder={@props.placeholder} />
+      <Error error={@state.errorMsg} />
+
+
+
+

+
+ExIdTags = React.createClass
+
+  propTypes:
+    exerciseId: React.PropTypes.string.isRequired
+
+  onAdd: ->
+    ExerciseActions.addBlankPrefixedTag(@props.exerciseId, prefix: PREFIX)
+
+  render: ->
+    tags = ExerciseStore.getTagsWithPrefix(@props.exerciseId, PREFIX)
+    <Wrapper label="LO" onAdd={@onAdd}>
+      {for tag in tags
+        <Input key={tag} {...@props} tag={tag} />}
+    </Wrapper>
+
+module.exports = ExIdTags


Support for nested/grouped attributes.

Support for nested/grouped attributes.

a href:'/login\', data: {icon:'arrow-r', iconpos:'right'}, 'Login' => Login

--- src/coffeekup.coffee
+++ src/coffeekup.coffee
@@ -140,15 +140,20 @@
           text c
         text '"'

-    render_attrs: (obj) ->
+    render_attrs: (obj, prefix = '') ->
       for k, v of obj
         # true is rendered as `selected="selected"`.
         if typeof v is 'boolean' and v
           v = k
+
+        if typeof v is 'object'
+          # if you have a lot of data attributes, it gets repetitive to keep typing 'data-'
+          # plus, it looks really good to group them together, e.g. data: { icon: 'gear', iconpos: 'right' }
+          @render_attrs(v, prefix + k + '-')
         # undefined, false and null result in the attribute not being rendered.
-        if v
+        else if v
           # strings, numbers, objects, arrays and functions are rendered "as is".
-          text " #{k}=\"#{@esc(v)}\""
+          text " #{prefix + k}=\"#{@esc(v)}\""

     render_contents: (contents) ->
       switch typeof contents


missing hyphen

missing hyphen

--- code/instance.coffee
+++ code/instance.coffee
@@ -110,7 +110,7 @@
   _scp: (key, files, callback) ->
     @_wait_for_sshd =>
       args = [
-        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', 'o', 'IdentitiesOnly=yes', '-i', key ]
+        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-o', 'IdentitiesOnly=yes', '-i', key ]
       args = args.concat files
       args.push "root@#{@ip_address}:/root"


moved key handling to textinputview

moved key handling to textinputview

--- core/app/backbone/views/autocompleted_add_to_channel_view.coffee
+++ core/app/backbone/views/autocompleted_add_to_channel_view.coffee
@@ -3,18 +3,31 @@
 class TextInputView extends Backbone.Marionette.ItemView
   events:
     'click': 'focusInput'
+    "keydown input.typeahead": "parseKeyDown"

   template:
     text: '<input type="text" class="typeahead">'

   focusInput: -> @$("input.typeahead").focus()

+  parseKeyDown: (e) ->
+    eventHandled = false
+    switch e.keyCode
+      when 13 then @trigger 'return'
+      when 40 then @trigger 'down'
+      when 38 then @trigger 'up'
+      when 27 then @trigger 'escape'
+      else eventHandled = true
+
+    unless eventHandled
+      e.preventDefault()
+      e.stopPropagation()
+

 class window.AutoCompletedAddToChannelView extends Backbone.Marionette.Layout
   tagName: "div"
   className: "add-to-channel"
   events:
-    "keydown input.typeahead": "parseKeyDown"
     "keyup input.typeahead": "autoCompleteCurrentValue"
     "click div.auto_complete": "addCurrentlySelectedChannel"
     "click div.fake-input a": "addCurrentlySelectedChannel"
@@ -36,24 +49,14 @@
       alreadyAdded: @collection
     @_text_input_view = new TextInputView()

+    @_text_input_view.on 'return', => @addCurrentlySelectedChannel()
+    @_text_input_view.on 'down', => @_auto_completes_view.moveSelectionDown()
+    @_text_input_view.on 'up', => @_auto_completes_view.moveSelectionUp()

   onRender: ->
     @added_channels.show @_added_channels_view
     @auto_completes.show @_auto_completes_view
     @input.show @_text_input_view
-
-  parseKeyDown: (e) ->
-    @_proceed = false
-    switch e.keyCode
-      when 13 then @addCurrentlySelectedChannel()
-      when 40 then @_auto_completes_view.moveSelectionDown()
-      when 38 then @_auto_completes_view.moveSelectionUp()
-      when 27 then @completelyDisappear()
-      else @_proceed = true
-
-    unless @_proceed
-      e.preventDefault()
-      e.stopPropagation()

   addCurrentlySelectedChannel: ->
     @disable()


adding multiline support for memories by replacing `.` with `[\S\s]` matching group to match new lines, since `.` doesnt match new lines in JavaScripts implementation of RegEx

--- src/scripts/remember.coffee
+++ src/scripts/remember.coffee
@@ -24,7 +24,7 @@

   robot.respond /rem(?:ember)?\s+(.*)/i, (msg) ->
     words = msg.match[1]
-    if match = words.match /(.*?)(\s+is\s+(.*))$/i
+    if match = words.match /(.*?)(\s+is\s+([\s\S]*))$/i
       msg.finish()
       key = match[1].toLowerCase()
       value = match[3]


the improved error messages are still-to-be-improved

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -25,8 +25,8 @@
 }

 # Improved error messages.
-parser.parseError: (message, hash) ->
-  throw new Error 'Unexpected ' + hash.token + ' on line ' + hash.line
+# parser.parseError: (message, hash) ->
+#   throw new Error 'Unexpected ' + parser.terminals_[hash.token] + ' on line ' + hash.line

 exports.VERSION: '0.5.0'


set isntMine class to stackbaseeditortabview and make editors read only

--- client/stacks/lib/views/stacks/stackbaseeditortabview.coffee
+++ client/stacks/lib/views/stacks/stackbaseeditortabview.coffee
@@ -7,11 +7,16 @@

   constructor: (options = {}, data) ->

-    if data
-      { stackTemplate } = data
-    unless stackTemplate?.isMine()
-      curryIn options, { cssClass: 'isntMine' }
-
     super options, data

+    kd.singletons.groupsController.ready =>
+      { groupsController } = kd.singletons
+      if data
+        { stackTemplate } = data
+        isMine = stackTemplate?.isMine() or groupsController.canEditGroup()
+        unless isMine
+          @setClass 'isntMine'
+          @editorView.aceView.ace.editor.once 'EditorReady', =>
+            @editorView.aceView.ace.editor.setReadOnly yes
+
     @on 'FocusToEditor', => @editorView?.setFocus yes


black Screen backgroundcolor

black Screen backgroundcolor

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -159,8 +159,7 @@

 			@setHand(@selectedHand) if @selectedHand && @_orientation == 0

-			@_context.backgroundColor = "white"
-			@_context.backgroundColor = @_device.backgroundColor if @_device.backgroundColor
+			@_context.backgroundColor = "black"

 	_shouldRenderFullScreen: ->

@@ -877,7 +876,6 @@
 	deviceImageJP2: true
 	screenWidth: 312
 	screenHeight: 390
-	backgroundColor: "black"
 	minStudioVersion: newDeviceMinVersion

 AppleWatch38Device =
@@ -886,7 +884,6 @@
 	deviceImageJP2: true
 	screenWidth: 272
 	screenHeight: 340
-	backgroundColor: "black"
 	minStudioVersion: newDeviceMinVersion

 AppleWatch38BlackLeatherDevice =
@@ -895,7 +892,6 @@
 	deviceImageJP2: true
 	screenWidth: 272
 	screenHeight: 340
-	backgroundColor: "black"
 	minStudioVersion: newDeviceMinVersion

 ###########################################################################


Don't draw "null", replace with ""

Don't draw "null", replace with ""

--- backbone.tableview.coffee
+++ backbone.tableview.coffee
@@ -71,7 +71,7 @@

                 <% _.each(columns, function (col, name) { %>
                     <td class="<%= col.className || "" %>">
-                        <%= col.draw ? col.draw(row) : row.get(name) %>
+                        <%= col.draw ? col.draw(row) : row.get(name) || "" %>

                 <% }) %>


inspecting message

inspecting message

--- app/assets/javascripts/building.coffee
+++ app/assets/javascripts/building.coffee
@@ -71,7 +71,7 @@
 				)
 		)

-		return if !!(window.history && window.history.pushState) # ie screws up all
+		return if not (window.history && window.history.pushState) # ie screws up all

 		history.replaceState("fixer","inspector","building")

@@ -150,13 +150,13 @@
 		$("#tweet").attr "href", twitterurl

 	animateSheet: () =>
-		return if @layer_id == @loadedData.map.layer_id
+		return if @layer_id == @loadedData.map.layer_id or @tutorialOn
 		@layer_id = @loadedData.map.layer_id
-		msg = "BROOKLYN"
-		msg = "MANHATTAN" if @layer_id == 859 # hack // eventually add to sheet table
+		msg = "Now inspecting:
Brooklyn, 1855"
+		msg = "Now inspecting:
Manhattan, 1857-62" if @layer_id == 859 # hack // eventually add to sheet table
 		el = $("#map-inspecting")
-		el.html("NOW INSPECTING " + msg + "!")
-		.show().delay(1000).fadeOut(1000)
+		el.html("" + msg + "")
+		.show().delay(2000).fadeOut(1000)

 	hideOthers: () ->
 		$("#main-container").hide()


forms.js redux

forms.js redux

--- app/assets/javascripts/forms.js.coffee
+++ app/assets/javascripts/forms.js.coffee
@@ -1,12 +1,25 @@
 App.Forms =

-  initialize: ->
+  disableEnter: ->
+    $('form.js-enter-disabled').on('keyup keypress', (event) ->
+      if event.which == 13
+        e.preventDefault()
+    )
+
+  submitOnChange: ->
     $('.js-submit-on-change').unbind('change').on('change', ->
       $(this).closest('form').submit()
       false
-      )
+    )

+  toggleLink: ->
     $('.js-toggle-link').unbind('click').on('click', ->
       $($(this).data('toggle-selector')).toggle("down")
       false
     )
+
+  initialize: ->
+    App.Forms.disableEnter()
+    App.Forms.submitOnChange()
+    App.Forms.toggleLink()
+    false


fix(macPopover): Fixed popover not changing from below to above when overflow

--- src/services/popover.coffee
+++ src/services/popover.coffee
@@ -275,7 +275,7 @@
           if offset.top + top - topScroll < 0 # above
             action = {remove: "above", add: "below"}

-          else if offset.top + top - topScroll > $window.height()
+          else if offset.top + top + current.height - topScroll > $window.height()
             action = {remove: "below", add: "above"}

         # if position is middle
@@ -286,6 +286,13 @@
           else if (diff = offset.top + top + currentPopover.outerHeight() - topScroll - $window.height()) > 0
             setOverflowPosition diff

+        # Update offset for above or below
+        if action.remove and action.add
+          position = position.replace action.remove, action.add
+
+        # Clear action for left and right
+        action = {}
+
         # Right align originally, switching to left
         if offset.left + left - leftScroll < 0
           action = {remove: "right", add: "left"}
@@ -296,7 +303,8 @@

         if action.remove and action.add
           position = position.replace action.remove, action.add
-          updateOffset()
+
+        updateOffset()

         offset.top  += top
         offset.left += left


postgres -- get/set project settings

postgres -- get/set project settings

--- src/smc-hub/postgres.coffee
+++ src/smc-hub/postgres.coffee
@@ -2214,13 +2214,38 @@
     Project settings
     ###
     get_project_settings: (opts) =>
-        throw Error("NotImplementedError")
+        opts = defaults opts,
+            project_id : required
+            cb         : required
+        @_query
+            query : "SELECT settings FROM projects"
+            where : 'project_id = $::UUID' : opts.project_id
+            cb    : one_result 'settings', (err, settings) =>
+                if err
+                    opts.cb(err)
+                else if not settings?
+                    opts.cb(undefined, misc.copy(DEFAULT_QUOTAS))
+                else
+                    settings = misc.coerce_codomain_to_numbers(settings)
+                    quotas = {}
+                    for k, v of DEFAULT_QUOTAS
+                        quotas[k] = if not settings[k]? then v else settings[k]
+                    opts.cb(undefined, quotas)

     set_project_settings: (opts) =>
-        throw Error("NotImplementedError")
+        opts = defaults opts,
+            project_id : required
+            settings   : required   # can be any subset of the map
+            cb         : required
+        @_query
+            query       : "UPDATE projects"
+            where       : 'project_id = $::UUID' : opts.project_id
+            jsonb_merge : {settings: opts.settings}
+            cb          : opts.cb

     count_timespan: (opts) =>
         throw Error("NotImplementedError")
+

 class SyncTable extends EventEmitter
     constructor: (@_query, @_primary_key, @_db, @_idle_timeout_s, cb) ->


refactored copy and paste functionality

refactored copy and paste functionality

--- app/assets/javascripts/snippets.coffee
+++ app/assets/javascripts/snippets.coffee
@@ -6,7 +6,7 @@
     $element.prepend('<button class="btn btn-default" data-clipboard-snippet><i class="fa fa-clipboard"></i></button>')

     clipboardSnippets = new Clipboard('[data-clipboard-snippet]', target: (trigger) ->
-      code = $(trigger).siblings( "code" ).get(0)
+      $(trigger).siblings( "code" ).get(0)
     )

     clipboardSnippets.on 'success', (e) ->
@@ -30,7 +30,4 @@
   return

 $ ->
-# -----------
-# Automation
-# -----------
   $('.snippet').initSnippetCopyToClipboard()


Allow dashes in subject codes

Allow dashes in subject codes

--- app/assets/javascripts/subjects.coffee
+++ app/assets/javascripts/subjects.coffee
@@ -75,7 +75,7 @@
           replace: (value) -> return "$1adverse-events:#{value}"
         },
         {
-          match: /(^|\s)(\w+)$/
+          match: /(^|\s)([\w\-]+)$/
           search: (term, callback) ->
             # callback(cache[term], true)
             $.getJSON("#{root_url}projects/#{$this.data('project-id')}/subjects/autocomplete", { q: term })


Fix js error

Fix js error

--- app/assets/javascripts/teams.js.coffee
+++ app/assets/javascripts/teams.js.coffee
@@ -15,7 +15,7 @@

   $('#add_namespace_btn').on 'click', (event) =>
     $('#add_namespace_form').fadeToggle 400, "swing", ->
-      if $('#add_namespace_form').css("display").is(':visible')
+      if $('#add_namespace_form').is(':visible')
         $('#add_namespace_btn i').addClass("fa-chevron-up")
         $('#add_namespace_btn i').removeClass("fa-chevron-down")
       else
@@ -24,7 +24,7 @@

   $('#add_team_btn').on 'click', (event) =>
     $('#add_team_form').fadeToggle 400, "swing", ->
-      if $('#add_team_form').css("display") == "block"
+      if $('#add_team_form').is(':visible')
         $('#add_team_btn i').addClass("fa-chevron-up")
         $('#add_team_btn i').removeClass("fa-chevron-down")
       else


Human readable date

Human readable date

--- app/assets/javascripts/views.js.coffee
+++ app/assets/javascripts/views.js.coffee
@@ -175,6 +175,7 @@
       @post.current = @post.creator_n == Blabbr.current_user.nickname
       @post.published = @post.state == 'published'
       @post.deleted = @post.state == 'deleted'
+      @post.created_at = new Date(@post.created_at).toDateString()
       super

     template: (post)->


billing: fix so will build via backend

billing: fix so will build via backend

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -38,7 +38,10 @@
 {powered_by_stripe} = require("./billing/util")
 {ProjectQuotaBoundsTable} = require("./billing/project-quota-bounds-table")
 {ProjectQuotaFreeTable} = require("./billing/project-quota-free-table")
-{Invoice} = require('./billing/invoice')
+if window.location?
+    # things that we won't use when doing backend rendering
+    # (this will go away when billing.cjsx is totally typescript'd)
+    {Invoice} = require('./billing/invoice')

 {Button, ButtonToolbar, FormControl, FormGroup, Row, Col, Accordion, Panel, Well, Alert, ButtonGroup, InputGroup} = require('react-bootstrap')
 {ActivityDisplay, CloseX, ErrorDisplay, Icon, Loading, SelectorInput, r_join, SkinnyError, Space, TimeAgo, Tip, Footer} = require('./r_misc')


correct view/play modes while navigating

--- app/coffee/game.coffee
+++ app/coffee/game.coffee
@@ -166,13 +166,22 @@
                  else if @state.proposedMove \
                       then @state.viewingMove + 1 \
                       else @state.viewingMove
-    scoringMode = (@state.viewingMove == null or
-                   @state.viewingMove == moveCount(origSgfObject)) and
-                  isPassedTwice sgfObject
+    isViewingLatestMove = @state.viewingMove == null or
+                          @state.viewingMove == actionCount(origSgfObject)
+    isOnTurn = tesuji_charm.onTurn and isViewingLatestMove
+    # show proposed scores in board view?
+    showScoring = isPassedTwice sgfObject, moves: moveToShow
+    # allow user to change stone markings?
+    scoringMode = isOnTurn and showScoring
+    # allow user to place a proposed new stone?
+    playMode = isOnTurn and not showScoring
+
     objState = stateFromSgfObject sgfObject,
                                   moves: moveToShow,
-                                  scoring: scoringMode
-                                  deadStones: @state.deadStones
+                                  scoring: showScoring,
+                                  deadStones: if scoringMode \
+                                              then @state.deadStones \
+                                              else null
     {boardState, lastPlayed, prisoners, scores} = objState
     size = parseInt(sgfObject.gameTrees[0].nodes[0].SZ, 10) or 19
     nextColor = null
@@ -188,9 +197,9 @@
                                                     deadStones: newDead
         newSgfObject = sgfObjectWithDeadStones origSgfObject, newState
         setResponseSgf smartgame.generate(newSgfObject)
-    else if (tesuji_charm.onTurn and \
-        (@state.viewingMove == null or \
-         @state.viewingMove == moveCount(origSgfObject)))
+        return
+
+    else if playMode
       # ready to accept a normal (non-dead stone marking) move
       nextColor = nextPlayerInSgfObject origSgfObject
       onBoardClick = (xy) =>
@@ -206,7 +215,9 @@
           color: nextColor
           x: xy.x
           y: xy.y
-    else
+        return
+
+    else  # not allowing user to make moves/mark dead right now
       onBoardClick = ->

     {div} = React.DOM
@@ -437,10 +448,8 @@
       prisoners.white += result.captures.white
   if scoring
     [x, y] = [null, null]
-    if deadFromUi?
-      deadStones = deadFromUi
-    else
-      deadStones = deadStonesFromSgfObject sgfObject, boardState
+    deadStones = deadFromUi ?
+                 deadStonesFromSgfObject sgfObject, boardState, moves: moves
     for [px, py] in deadStones
       switch boardState[py][px]
         when 'black'
@@ -454,11 +463,15 @@
     scores = null
   {boardState, lastPlayed: {x, y}, prisoners, scores}

-deadStonesFromSgfObject = (sgfObject, state) ->
+deadStonesFromSgfObject = (sgfObject, state, options={}) ->
   "Read current territories in sgfObject, mark dead stones in `state`
   accordingly, and return an array of points for all dead stones."
+  {moves} = options
   nodes = sgfObject.gameTrees[0].nodes
-  node = nodes[nodes.length-1]
+  nodeIndex = if moves \
+              then nodeNoFromActionNo sgfObject, moves \
+              else nodes.length - 1
+  node = nodes[nodeIndex]
   deadStones = []
   for tag in node.TB or []
     [x, y] = decodeSgfCoord tag
@@ -519,11 +532,23 @@
         regions.push region
   return regions

-moveCount = (sgfObject) ->
+isActionNode = (node) -> node.B? or node.W? or node.TB? or node.TW?
+
+nodeNoFromActionNo = (sgfObject, actionNo) ->
+  "find the node index corresponding to the given action number"
+  nodes = sgfObject.gameTrees[0].nodes
+  actionsToGo = actionNo
+  for node, i in nodes
+    if isActionNode(node)
+      actionsToGo -= 1
+    break if actionsToGo <= 0
+  return i
+
+actionCount = (sgfObject) ->
   "Return number of moves (including passes) in sgf object."
   count = 0
   for node in sgfObject.gameTrees[0].nodes
-    if node.B? or node.W? or node.TW? or node.TB?
+    if isActionNode(node)
       count += 1
   count

@@ -592,16 +617,21 @@
     throw Error "no passes found before resumption node"
   return lastPassSeen

-isPassedTwice = (sgfObject) ->
-  # true iff sgf ends with two successive passes
+isPassedTwice = (sgfObject, options={}) ->
+  "true iff sgf ends with two successive passes
+
+  if `moves` is given, limit the game to that many moves."
+  {moves} = options
   nodes = sgfObject.gameTrees[0].nodes
-  len = nodes.length
+  nodeIndex = if moves \
+              then nodeNoFromActionNo sgfObject, moves \
+              else nodes.length - 1
   isPass = (node) -> node.B == '' or node.W == ''
   hasMoveOrResume = (node) -> (typeof node.B == 'string' and node.B != '') or
                               (typeof node.W == 'string' and node.W != '') or
                               node.TCRESUME?
   passesSeen = 0
-  i = len - 1
+  i = nodeIndex
   while i >= 0 and not hasMoveOrResume(nodes[i])
     if isPass(nodes[i])
       passesSeen += 1


Make test expectation clearer

Make test expectation clearer

--- test/equation_builder_spec.coffee
+++ test/equation_builder_spec.coffee
@@ -182,4 +182,6 @@

     solution = solve()

-    expect(solution()).to.eql [[100], [100], [1]] # 100 volts, 1 amp
+    expect(solution()).to.eql [[100] # volts
+                               [100] # volts
+                               [1]]  # amp


Added stack trace to optional argument error. (cherry picked from commit 833c657)

--- src/contracts.coffee
+++ src/contracts.coffee
@@ -256,7 +256,8 @@
         true
       else
         if prevWasOpt
-          throw new Error "Illagal arguments: required argument following an optional argument."
+          throw new Error "Illagal arguments: required argument following an" +
+            "optional argument. #{curr}"
         else
           false
     ), false


Remount Virtual Env whenever kill a mount process

--- kites/sharedHosting/mounter.coffee
+++ kites/sharedHosting/mounter.coffee
@@ -630,9 +630,10 @@
             exec "/usr/bin/pkill #{args}", (err)=>
               # console.log "KILLING THEM ALL"
               console.log "Mount procceses killed for #{username}" unless err
-              cleanUpRemotesDir =>
-                mkdirp remotesPath, 0o0755, (err)->
-                  callback "Process tree cleaned-up and RemoteDrives re-created."
+              @remountVE {username}, =>
+                cleanUpRemotesDir =>
+                  mkdirp remotesPath, 0o0755, (err)->
+                    callback "Process tree cleaned-up and RemoteDrives re-created."
               # callback()
           else
             cleanUpRemotesDir =>


preserve existing metric name

preserve existing metric name

--- services/clsi/app/coffee/CompileManager.coffee
+++ services/clsi/app/coffee/CompileManager.coffee
@@ -54,8 +54,10 @@
 					Metrics.gauge("load-avg", loadavg[0]) if loadavg?
 					ts = timer.done()
 					logger.log {project_id: request.project_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile"
+					if stats?["latex-runs"] > 0
+						Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"])
 					if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0
-						Metrics.timing("run-compile-per-pass", timings["cpu-time"] / stats["latex-runs"])
+						Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"])

 					OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) ->
 						return callback(error) if error?


Simplify GCD and LCM.

Simplify GCD and LCM.

--- src/vocabulary/logic.coffee
+++ src/vocabulary/logic.coffee
@@ -51,12 +51,11 @@
     # 299∨323           <=> 1   # 299=13×23, 323=17×19
     # 12345∨12345       <=> 12345
     # 0∨123             <=> 123
+    # 123∨0             <=> 123
     # ∨/⍬               <=> 0
     dyad: real (y, x) ->
       if not (isInt(x, 0) and isInt(y, 0))
         throw DomainError '∨ is implemented only for non-negative integers' # todo
-      if x is 0 and y is 0 then return 0
-      if x < y then [x, y] = [y, x]
       while y then [x, y] = [y, x % y] # Euclid's algorithm
       x

@@ -77,12 +76,12 @@
     # 299∧323 # 299=13×23, 323=17×19 <=> 96577
     # 12345∧12345                    <=> 12345
     # 0∧123                          <=> 0
+    # 123∧0                          <=> 0
     # ∧/⍬                            <=> 1
     dyad: real (y, x) ->
       assert x is Math.floor(x) and y is Math.floor(y), '∧ is defined only for integers'
-      if x is 0 or y is 0 then return 0
       p = x * y
-      if x < y then [x, y] = [y, x]
+      if p is 0 then return 0
       while y then [x, y] = [y, x % y] # Euclid's algorithm
       p / x # LCM(x, y) = x * y / GCD(x, y)


Don't enable long stack traces by default

--- src/web-client/index.coffee
+++ src/web-client/index.coffee
@@ -1,5 +1,5 @@
 Promise = require 'bluebird'
-Promise.longStackTraces()
+# Promise.longStackTraces()

 log = require '../log'


use booleans to toggle tutorial

use booleans to toggle tutorial

--- app/assets/javascripts/components/mark/index.cjsx
+++ app/assets/javascripts/components/mark/index.cjsx
@@ -44,7 +44,7 @@
     lightboxHelp:        false
     activeSubjectHelper: null
     subjectCurrentPage:  1
-    yaleTutorial:        1
+    yaleTutorial:        true

   componentWillReceiveProps: (new_props) ->
     @setState yaleTutorial: @showTutorialBasedOnUser(new_props.user)
@@ -97,12 +97,13 @@
     @createAndCommitClassification(annotation)

   toggleYaleTutorial: ->
-    if @state.yaleTutorial == 0
-      @setState({yaleTutorial: 1})
+    if @state.yaleTutorial == false
+      @setState({yaleTutorial: true})
     else
       # store the fact that this user has completed the tutorial
       @props.onCloseTutorial()
-      @setState({yaleTutorial: 0})
+      @setState({yaleTutorial: false})
+    console.log(@state.yaleTutorial)

   # Handle user selecting a pick/drawing tool:
   handleDataFromTool: (d) ->


[#75632076] Fix unbinding of papersDidLoad event

--- app/assets/javascripts/views/index_view.js.coffee
+++ app/assets/javascripts/views/index_view.js.coffee
@@ -1,13 +1,14 @@
 ETahi.IndexView = Ember.View.extend
   listenForPapers: (->
     @get('controller').on 'papersDidLoad', this, =>
+      console.log 'listenForPapers fired'
       Em.run.later ( => @setupTooltips() ), 150

     @setupTooltips()
   ).on('didInsertElement')

   stopListeningForPapers: (->
-    @get('controller').off('papersDidLoad', @, @setupTooltips)
+    @get('controller').off('papersDidLoad', @)
   ).on('willDestroyElement')

   setupTooltips: ->


Rename Editor to EditorView

Rename Editor to EditorView

--- lib/package-generator-view.coffee
+++ lib/package-generator-view.coffee
@@ -1,4 +1,4 @@
-{_, $, BufferedNodeProcess, Editor, fs, View} = require 'atom'
+{_, $, BufferedNodeProcess, EditorView, fs, View} = require 'atom'
 path = require 'path'

 module.exports =
@@ -8,7 +8,7 @@

   @content: ->
     @div class: 'package-generator overlay from-top', =>
-      @subview 'miniEditor', new Editor(mini: true)
+      @subview 'miniEditor', new EditorView(mini: true)
       @div class: 'error', outlet: 'error'
       @div class: 'message', outlet: 'message'


Removed minute calculation in KDTimeAgoView.

--- components/time/timeagoview.coffee
+++ components/time/timeagoview.coffee
@@ -8,13 +8,7 @@

     super options, data

-    KDTimeAgoView.on "OneMinutePassed", =>
-      oldDate   = new Date @getData()
-      timestamp = oldDate.setMinutes oldDate.getMinutes() - 1
-
-      @setData dateFormat timestamp, "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
-
-      @updatePartial $.timeago @getData()
+    KDTimeAgoView.on "OneMinutePassed", => @updatePartial $.timeago @getData()

   viewAppended: ->
     @setPartial $.timeago @getData()


Fix spawning and closing terminal.

Fix spawning and closing terminal.

--- lib/rubinius-terminal-view.coffee
+++ lib/rubinius-terminal-view.coffee
@@ -50,7 +50,9 @@
         "HOME": process.env.HOME
       stdio: 'pipe'

-    @ptyProcess = spawn "/Users/bshirai/.rubies/rbx-2.4.1/bin/rbx", [pty, @opts.shell], options
+    @ptyProcess = spawn rbx, [pty, @opts.shell], options
+
+    @ptyProcess.on 'exit', (code, signal) => @destroy()
     [@ptyRead, @ptyWrite] = [@ptyProcess.stdin, @ptyProcess.stdout]

     @ptyProcess
@@ -63,7 +65,6 @@
     @createPTY args

     @ptyWrite.on 'data', (data) =>
-      console.log "stdout: #{typeof(data).toString()}, #{data}"
       @terminal.write data.toString()

     # TODO: investigate this lifetime
@@ -160,7 +161,6 @@

   destroy: ->
     @detachResizeEvents()
-    @ptyProcess.terminate()
     @terminal.destroy()
     parentPane = atom.workspace.getActivePane()
     if parentPane.activeItem is this


Remove unneeded componentWillUnmount method

It's defined in the SubscriberMixin

--- src/gutter-component.coffee
+++ src/gutter-component.coffee
@@ -41,9 +41,6 @@
     div className: 'line-numbers', style: style,
       lineNumbers

-  componentWillUnmount: ->
-    @unsubscribe()
-
   # Only update the gutter if the visible row range has changed or if a
   # non-zero-delta change to the screen lines has occurred within the current
   # visible row range.


Kill comment

Kill comment

--- src/gutter-component.coffee
+++ src/gutter-component.coffee
@@ -162,7 +162,6 @@

   measureWidth: ->
     lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild
-    # return unless lineNumberNode?

     width = lineNumberNode.offsetWidth
     if width isnt @lastMeasuredWidth


remove quotes from fuzzy search in URL

remove quotes from fuzzy search in URL

--- src/hubot-confluence.coffee
+++ src/hubot-confluence.coffee
@@ -40,9 +40,9 @@
   timeout = nconf.get("HUBOT_CONFLUENCE_TIMEOUT") or 2000
   space = nconf.get("HUBOT_CONFLUENCE_SEARCH_SPACE")
   if text
-    text_search = "text~\"#{query}\""
+    text_search = "text~#{query}"
   else
-    text_search = "title~\"#{query}\""
+    text_search = "title~#{query}"

   query_str = "type=page+and+space+in(#{space})+and+#{text_search}"
   query_str =  encodeURIComponent query_str


Fix storage.database is closed before db.flush

--- app/src/utils/database/database.coffee
+++ app/src/utils/database/database.coffee
@@ -3,11 +3,12 @@

 class Database extends EventEmitter

-  constructor: (name) ->
+  constructor: (name, store) ->
     @_name = name
+    @_store = store

   load: (callback) ->
-    ledger.storage.databases.get @_name, (json) =>
+    @_store.get @_name, (json) =>
       try
         @_db = new loki(@_name)
         @_db.loadJSON JSON.stringify(json[@_name]) if json[@_name]?
@@ -28,7 +29,7 @@
       clearTimeout @_scheduledFlush if @_scheduledFlush?
       serializedData = {}
       serializedData[@_name] = @_db
-      ledger.storage.databases.set serializedData, callback
+      @_store.set serializedData, callback

   scheduleFlush: () ->
     clearTimeout @_scheduledFlush if @_scheduledFlush?
@@ -54,7 +55,7 @@
       callback?()

   open: (databaseName, callback) ->
-    db = new Database databaseName
+    db = new Database(databaseName, ledger.storage.databases)
     db.load callback
     db


Call domLoaded if the loader is downloaded when the dom is already loaded

--- src/moka/applicationLoader.coffee
+++ src/moka/applicationLoader.coffee
@@ -107,4 +107,9 @@
       forceAppLoading_ = yes

   customAPI()
-  bindReady()
+  if document.readyState is "complete"
+    # document is already laoded, fire callback
+    domDidLoad()
+  else
+    bindReady()
+