Browse Source

fixes #7: add notes to attendees, groups and days

Davide Alberani 7 years ago
parent
commit
9657cb17f0
9 changed files with 244 additions and 15 deletions
  1. 6 0
      build/webpack.dev.conf.js
  2. 6 0
      build/webpack.prod.conf.js
  3. 2 1
      config/index.js
  4. 1 1
      docs/DEVELOPMENT.md
  5. 35 4
      ibt2.py
  6. 74 2
      src/App.vue
  7. 91 6
      src/Group.vue
  8. 0 1
      src/main.js
  9. 29 0
      tests/ibt2_tests.py

+ 6 - 0
build/webpack.dev.conf.js

@@ -17,6 +17,12 @@ module.exports = merge(baseWebpackConfig, {
   // eval-source-map is faster for development
   devtool: '#eval-source-map',
   plugins: [
+    new webpack.ProvidePlugin({
+        $: 'jquery',
+        jquery: 'jquery',
+        'window.jQuery': 'jquery',
+        jQuery: 'jquery'
+    }),
     new webpack.DefinePlugin({
       'process.env': config.dev.env
     }),

+ 6 - 0
build/webpack.prod.conf.js

@@ -25,6 +25,12 @@ var webpackConfig = merge(baseWebpackConfig, {
     })
   },
   plugins: [
+    new webpack.ProvidePlugin({
+        $: 'jquery',
+        jquery: 'jquery',
+        'window.jQuery': 'jquery',
+        jQuery: 'jquery'
+    }),
     // http://vuejs.github.io/vue-loader/en/workflow/production.html
     new webpack.DefinePlugin({
       'process.env': env

+ 2 - 1
config/index.js

@@ -26,7 +26,8 @@ module.exports = {
         '/attendees': 'http://localhost:3000',
         '/login': 'http://localhost:3000',
         '/logout': 'http://localhost:3000',
-        '/user': 'http://localhost:3000'
+        '/user': 'http://localhost:3000',
+        '/groups': 'http://localhost:3000'
     },
     // CSS Sourcemaps off by default because relative paths are "buggy"
     // with this option, according to the CSS-Loader README

+ 1 - 1
docs/DEVELOPMENT.md

@@ -38,7 +38,7 @@ Example of */attendees/:id*:
 
 Example of */days/:day*:
 ``` json
-{"day": "2017-01-20", "groups": [{"name": "Group Name", "attendees": [{"day": "2017-01-20", "name": "Attendee Name", "group": "Group Name", "updated_by": "587a7c79dff0d71c89211dc4", "created_at": "2017-01-20 13:57:26.029000", "updated_at": "2017-01-20 13:57:26.029000", "created_by": "587a7c79dff0d71c89211dc4", "_id": "58820936dff0d740dee647a4"}]}]}
+{"day": "2017-01-20", "groups": [{"group": "Group Name", "attendees": [{"day": "2017-01-20", "name": "Attendee Name", "group": "Group Name", "updated_by": "587a7c79dff0d71c89211dc4", "created_at": "2017-01-20 13:57:26.029000", "updated_at": "2017-01-20 13:57:26.029000", "created_by": "587a7c79dff0d71c89211dc4", "_id": "58820936dff0d740dee647a4"}]}]}
 ```
 
 

+ 35 - 4
ibt2.py

@@ -270,8 +270,12 @@ class DaysHandler(BaseHandler):
         end = params.get('end')
         if end:
             del params['end']
+        base = {}
+        groupsDetails = {}
         if day:
             params['day'] = day
+            base = self.db.getOne('days', {'day': day})
+            groupsDetails = dict([(x['group'], x) for x in self.db.query('groups', {'day': day})])
         else:
             if start:
                 params['day'] = {'$gte': start}
@@ -299,18 +303,42 @@ class DaysHandler(BaseHandler):
                 for group, attendees in itertools.groupby(sorted(dayItems, key=itemgetter('group')),
                                                           key=itemgetter('group')):
                     attendees = sorted(attendees, key=itemgetter('_id'))
-                    dayData['groups'].append({'group': group, 'attendees': attendees})
+                    groupData = groupsDetails.get(group) or {}
+                    groupData.update({'group': group, 'attendees': attendees})
+                    dayData['groups'].append(groupData)
                 days.append(dayData)
         except Exception as e:
-            self.logger.warn('unable to parse entry; dayData: %s', dayData)
+            self.logger.warn('unable to parse entry; dayData: %s error: %s', dayData, e)
         if summary:
             days = self._summarize(days)
         if not day:
             self.write({'days': days})
         elif days:
-            self.write(days[0])
+            base.update(days[0])
+            self.write(base)
         else:
-            self.write({})
+            self.write(base)
+
+    @gen.coroutine
+    def put(self, **kwargs):
+        data = self.clean_body
+        now = datetime.datetime.now()
+        data['updated_by'] = self.current_user_info.get('_id')
+        data['updated_at'] = now
+        merged, doc = self.db.update('days', self.arguments, data)
+        self.write(doc)
+
+
+class GroupsHandler(BaseHandler):
+    """Handle requests for Groups."""
+    @gen.coroutine
+    def put(self, **kwargs):
+        data = self.clean_body
+        now = datetime.datetime.now()
+        data['updated_by'] = self.current_user_info.get('_id')
+        data['updated_at'] = now
+        merged, doc = self.db.update('groups', self.arguments, data)
+        self.write(doc)
 
 
 class UsersHandler(BaseHandler):
@@ -504,6 +532,7 @@ def run():
                 {'setting': 'server_cookie_secret', 'cookie_secret': cookie_secret})
 
     _days_path = r"/days/?(?P<day>[\d_-]+)?"
+    _groups_path = r"/groups/?"
     _attendees_path = r"/attendees/?(?P<id_>[\w\d_-]+)?"
     _current_user_path = r"/users/current/?"
     _users_path = r"/users/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
@@ -511,6 +540,8 @@ def run():
             (_attendees_path, AttendeesHandler, init_params),
             (r'/v%s%s' % (API_VERSION, _attendees_path), AttendeesHandler, init_params),
             (_days_path, DaysHandler, init_params),
+            (r'/v%s%s' % (API_VERSION, _groups_path), GroupsHandler, init_params),
+            (_groups_path, GroupsHandler, init_params),
             (r'/v%s%s' % (API_VERSION, _days_path), DaysHandler, init_params),
             (_current_user_path, CurrentUserHandler, init_params),
             (r'/v%s%s' % (API_VERSION, _current_user_path), CurrentUserHandler, init_params),

+ 74 - 2
src/App.vue

@@ -3,6 +3,29 @@
         <md-layout md-gutter md-row>
             <md-layout id="datepicker-column" md-column md-flex="20" md-gutter>
                 <datepicker id="datepicker" :value="date" :inline="true" :highlighted="highlightedDates" @selected="getDay"></datepicker>
+                <md-card id="day-info">
+                    <md-card-header class="group-header">
+                        <md-layout md-row>
+                            <div class="md-title day-info-title">
+                                <md-icon class="day-icon">today</md-icon>&nbsp;{{ day.day }}
+                            </div>
+                            <md-menu md-align-trigger>
+                                <md-button class="md-icon-button" md-menu-trigger>
+                                    <md-icon>more_vert</md-icon>
+                                </md-button>
+                                <md-menu-content>
+                                    <md-menu-item @click="openNotesDialog()">
+                                        <span>edit notes</span>
+                                        <md-icon>edit</md-icon>
+                                    </md-menu-item>
+                                </md-menu-content>
+                            </md-menu>
+                        </md-layout>
+                    </md-card-header>
+                    <md-card-content>
+                        <div id="day-notes">{{ day.notes }}</div>
+                    </md-card-content>
+                </md-card>
             </md-layout>
             <md-layout id="panel" md-column>
                 <md-layout md-row>
@@ -12,6 +35,15 @@
             </md-layout>
         </md-layout>
         <ibt-dialog ref="dialogObj" />
+        <md-dialog-prompt
+                    v-model="dayNotes"
+                    @open="dialogDayNotesOpen"
+                    @close="dialogDayNotesClose"
+                    :md-title="noteDialog.title"
+                    :md-ok-text="noteDialog.ok"
+                    :md-cancel-text="noteDialog.cancel"
+                    ref="dialogDayNotes">
+        </md-dialog-prompt>
     </div>
 </template>
 <script>
@@ -25,7 +57,9 @@ export default {
         return {
             date: null, // a Date object representing the selected date
             day: {},
-            daysSummary: {}
+            daysSummary: {},
+            dayNotes: '',
+            noteDialog: {title: 'Day notes', ok: 'ok', cancel: 'cancel'}
         }
     },
 
@@ -123,6 +157,28 @@ export default {
                 }
                 this.day = dayData;
             });
+        },
+
+        openNotesDialog() {
+            this.$refs.dialogDayNotes.open();
+        },
+
+        dialogDayNotesOpen() {
+            this.dayNotes = this.day.notes || '';
+        },
+
+        dialogDayNotesClose(type) {
+            if (type != 'ok' || !this.day) {
+                return;
+            }
+            var data = {day: this.day.day, notes: this.dayNotes};
+            this.daysUrl.update(data).then((response) => {
+                return response.json();
+            }, (response) => {
+                this.$refs.dialogObj.show({text: 'unable to edit day notes'});
+            }).then((json) => {
+                this.day.notes = json.notes;
+            });
         }
     },
 
@@ -154,8 +210,24 @@ export default {
     flex: initial;
 }
 
-#toolbar-title {
+#day-info {
+    margin: 10px;
+    width: 300px;
+    min-height: 200px;
+}
+
+.day-info-title {
     flex: 1;
 }
 
+#day-notes {
+    padding: 10px;
+    color: rgba(0, 0, 0, 0.54);
+}
+
+.day-icon {
+    vertical-align: text-top;
+}
+
+
 </style>

+ 91 - 6
src/Group.vue

@@ -4,9 +4,23 @@
         <md-card v-if="!addNewGroup" md-with-hover @mouseenter.native="focusToNewAttendee()">
             <md-card-header class="group-header">
                 <md-layout md-row>
-                    <div class="md-title">
+                    <div class="md-title group-title">
                         <md-icon class="group-icon">folder_open</md-icon>&nbsp;Group: {{ group.group }}&nbsp;<span class="counter">{{ counter }}</span>
                     </div>
+                    <md-menu md-align-trigger>
+                        <md-button class="md-icon-button" md-menu-trigger>
+                            <md-icon>more_vert</md-icon>
+                        </md-button>
+                        <md-menu-content>
+                            <md-menu-item @click="openNotesDialog()">
+                                <span>edit notes</span>
+                                <md-icon>edit</md-icon>
+                            </md-menu-item>
+                        </md-menu-content>
+                    </md-menu>
+                </md-layout>
+                <md-layout v-if="group.notes" md-row>
+                    <div ref="groupNotes" class="group-notes" @click="toggleNotes()">{{ group.notes }}</div>
                 </md-layout>
             </md-card-header>
             <md-card-content>
@@ -35,9 +49,10 @@
         </md-card>
         <md-card v-if="addNewGroup" md-with-hover @mouseenter.native="focusToNewGroup()" md-align="start">
             <md-card-header class="new-group-header">
-                <div class="md-title">
-                    <md-input-container class="new-group" md-inline>
-                        <md-icon>create_new_folder</md-icon>&nbsp;&nbsp;<md-input ref="newGroup" v-model="newGroup" @keyup.enter.native="focusToNewAttendee()" class="group-add-name" placeholder="new group" />
+                <div class="md-title group-title">
+                    <md-input-container class="new-group">
+                        <label class="new-group-label">new group</label>
+                        <md-icon>create_new_folder</md-icon>&nbsp;&nbsp;<md-input ref="newGroup" v-model="newGroup" @keyup.enter.native="focusToNewAttendee()" class="group-add-name" />
                     </md-input-container>
                 </div>
             </md-card-header>
@@ -65,6 +80,15 @@
             </md-card-content>
         </md-card>
         <ibt-dialog ref="dialogObj" />
+        <md-dialog-prompt
+                    v-model="groupNotes"
+                    @open="dialogGroupNotesOpen"
+                    @close="dialogGroupNotesClose"
+                    :md-title="noteDialog.title"
+                    :md-ok-text="noteDialog.ok"
+                    :md-cancel-text="noteDialog.cancel"
+                    ref="dialogGroupNotes">
+        </md-dialog-prompt>
     </md-layout>
 </template>
 <script>
@@ -79,7 +103,10 @@ export default {
         return {
             newAttendee: '',
             newAttendeeNotes: '',
-            newGroup: ''
+            newGroup: '',
+            groupNotes: '',
+            noteDialog: {title: 'Group notes', ok: 'ok', cancel: 'cancel'},
+            expandedNote: false
         }
     },
 
@@ -90,6 +117,7 @@ export default {
     },
 
     beforeCreate: function() {
+        this.groupsUrl = this.$resource('groups');
         this.attendeesUrl = this.$resource('attendees{/id}');
     },
 
@@ -129,6 +157,41 @@ export default {
                 this.reset();
                 this.$emit('updated');
             });
+        },
+
+        openNotesDialog() {
+            this.$refs.dialogGroupNotes.open();
+        },
+
+        dialogGroupNotesOpen() {
+            this.groupNotes = this.group.notes || '';
+        },
+
+        dialogGroupNotesClose(type) {
+            if (type != 'ok' || !this.group || !this.group.group || !this.day) {
+                return;
+            }
+            var data = {day: this.day, group: this.group.group, notes: this.groupNotes};
+            this.groupsUrl.update(data).then((response) => {
+                return response.json();
+            }, (response) => {
+                this.$refs.dialogObj.show({text: 'unable to edit group notes'});
+            }).then((json) => {
+                this.reset();
+                this.$emit('updated');
+            });
+        },
+
+        toggleNotes() {
+            if (!this.expandedNote) {
+                $(this.$refs.groupNotes).css('text-overflow', 'initial');
+                $(this.$refs.groupNotes).css('white-space', 'initial');
+                this.expandedNote = true;
+            } else {
+                $(this.$refs.groupNotes).css('text-overflow', 'ellipsis');
+                $(this.$refs.groupNotes).css('white-space', 'nowrap');
+                this.expandedNote = false;
+            }
         }
     },
 
@@ -148,7 +211,11 @@ export default {
     padding-bottom: 0px !important;
 }
 
-.new-group-header .md-title {
+.group-title {
+    flex: 1;
+}
+
+.new-group-header .group-title {
     margin-top: 0px !important;
 }
 
@@ -189,4 +256,22 @@ export default {
     max-width: 120px;
 }
 
+.group-notes {
+    font-style: italic;
+    padding-left: 15px;
+    text-overflow: ellipsis;
+    max-width: 400px;
+    overflow: hidden;
+    white-space: nowrap;
+    color: rgba(0, 0, 0, 0.54);
+}
+
+.new-group-label {
+    left: 30px !important;
+}
+
+.group-add-name {
+    margin-left: 0px !important;
+}
+
 </style>

+ 0 - 1
src/main.js

@@ -22,7 +22,6 @@ import VueMaterial from 'vue-material';
 import 'vue-material/dist/vue-material.css';
 import 'roboto-fontface/css/roboto/roboto-fontface.css';
 import 'material-design-icons/iconfont/material-icons.css';
-import jQuery from 'jquery';
 import store_data from './store.js';
 import App from './App';
 import User from './User';

+ 29 - 0
tests/ibt2_tests.py

@@ -37,11 +37,15 @@ class Ibt2Tests(unittest.TestCase):
         self.connection = self.monco_conn.connection
         self.db = self.monco_conn.db
         self.db['attendees'].drop()
+        self.db['days'].drop()
+        self.db['groups'].drop()
         self.db['users'].remove({'username': 'newuser'})
         self.db['users'].remove({'username': 'newuser2'})
 
     def tearDown(self):
         self.db['attendees'].drop()
+        self.db['days'].drop()
+        self.db['groups'].drop()
         self.db['users'].remove({'username': 'newuser'})
         self.db['users'].remove({'username': 'newuser2'})
 
@@ -180,5 +184,30 @@ class Ibt2Tests(unittest.TestCase):
         self.assertEqual(user_id, rj['created_by'])
         self.assertEqual(user_id, rj['updated_by'])
 
+    def test_put_day(self):
+        day = {'day': '2017-01-16', 'notes': 'A day note'}
+        self.add_attendee({'day': '2017-01-16', 'name': 'A new name', 'group': 'group C'})
+        r = requests.put(BASE_URL + 'days', json=day)
+        r.raise_for_status()
+        rj = r.json()
+        self.assertTrue(dictInDict(day, rj))
+        r = requests.get(BASE_URL + 'days/2017-01-16')
+        r.raise_for_status()
+        rj = r.json()
+        self.assertTrue(dictInDict(day, rj))
+
+    def test_put_group(self):
+        self.add_attendee({'day': '2017-01-16', 'name': 'A new name', 'group': 'A group'})
+        group = {'group': 'A group', 'day': '2017-01-16', 'notes': 'A group note'}
+        r = requests.put(BASE_URL + 'groups', json=group)
+        r.raise_for_status()
+        rj = r.json()
+        self.assertTrue(dictInDict(group, rj))
+        r = requests.get(BASE_URL + 'days/2017-01-16')
+        r.raise_for_status()
+        rj = r.json()
+        self.assertTrue(dictInDict(group, rj['groups'][0]))
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)