Ejemplo n.º 1
0
        public async Task <IHttpActionResult> PostFinishReview(int reviewId)
        {
            var userId = this.GetUserId();

            // Get ExerciseId and FinishTime back.
            var output = (await DapperHelper.QueryResilientlyAsync <dynamic>(
                              "dbo.exeFinishReview",
                              new
            {
                ReviewId = reviewId,
                UserId = userId,
            },
                              CommandType.StoredProcedure))
                         .Single();

            var partitionKey = ReviewPiece.GetPartitionKey(output.ExerciseId);
            // Revoke write access from the reviewer.
            var reviewerEntity = new ReviewPiece()
            {
                PartitionKey = partitionKey,
                RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId),
                ETag         = "*",
            };
            var operation = TableOperation.Delete(reviewerEntity);
            var table     = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
            await table.ExecuteAsync(operation);

            var finishTime = DateTime.SpecifyKind(output.FinishTime, DateTimeKind.Utc);

            // Notify the watching exercise author
            // this.GetAuthorConnections(partitionKey).ReviewFinished(reviewId, finishTime);

            return(Ok(new { FinishTime = finishTime }));
        }
Ejemplo n.º 2
0
        public async Task <IHttpActionResult> GetReviewPieces(int exerciseId, int reviewId)
        {
            var    filterPartition   = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, ReviewPiece.GetPartitionKey(exerciseId));
            var    filterRowFrom     = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, KeyUtils.IntToKey(reviewId));
            var    filterRowTo       = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, KeyUtils.IntToKey(reviewId) + "Z");
            string combinedRowFilter = TableQuery.CombineFilters(filterRowFrom, TableOperators.And, filterRowTo);
            string combinedFilter    = TableQuery.CombineFilters(filterPartition, TableOperators.And, combinedRowFilter);
            var    query             = new TableQuery <ReviewPiece>().Where(combinedFilter);
            var    pieces            = await AzureStorageUtils.ExecuteQueryAsync(AzureStorageUtils.TableNames.ReviewPieces, query);

            // The access entity was written at the review start. See ReviewsApiController.PostStartReview
            var userRowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, this.GetUserId());

            if (pieces.Any(i => i.RowKey == userRowKey))
            {
                RemoveAccessEntries(pieces);
            }
            else
            {
                pieces.Clear();
            }

            var piecesArr = pieces.Select(i => i.Json).ToArray();

            return(Ok(piecesArr));
        }
Ejemplo n.º 3
0
        private async Task <bool> UserIsEditor(string partitionKey, int reviewId, CloudTable table)
        {
            var rowKey    = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, this.GetUserId());
            var operation = TableOperation.Retrieve <ReviewPiece>(partitionKey, rowKey);
            var result    = await table.ExecuteAsync(operation);

            return(result.Result != null);
        }
Ejemplo n.º 4
0
 private void RemoveAccessEntries(List <ReviewPiece> pieces)
 {
     // Remove access entries from the list.
     pieces.RemoveAll(i =>
     {
         var type = ReviewPiece.GetType(i.RowKey);
         return((type == ReviewPiece.PieceTypes.Editor) || (type == ReviewPiece.PieceTypes.Viewer));
     });
 }
Ejemplo n.º 5
0
        public async Task <IHttpActionResult> PostStartReview(int reviewId)
        {
            var userId = this.GetUserId();

            // Get ExerciseId, AuthorUserId, StartTime back.
            var output = (await DapperHelper.QueryResilientlyAsync <dynamic>("dbo.exeStartReview",
                                                                             new
            {
                UserId = userId,
                ReviewId = reviewId,
            },
                                                                             CommandType.StoredProcedure))
                         .Single();

            /* Write entities which will allow the reviewer to access for reading and writing and the author for reading.
             * We will simply check the presence of one of these records as we read or write the entities.
             * The write entity will be deleted on review finish.
             */
            var partitionKey = ReviewPiece.GetPartitionKey(output.ExerciseId);
            var viewerEntity = new ReviewPiece()
            {
                PartitionKey = partitionKey,
                RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId),
            };
            var editorEntity = new ReviewPiece()
            {
                PartitionKey = partitionKey,
                RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId),
            };
            var authorEntity = new ReviewPiece()
            {
                PartitionKey = partitionKey,
                RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, output.AuthorUserId),
            };

            var batchOperation = new TableBatchOperation();

            batchOperation.InsertOrReplace(viewerEntity);
            batchOperation.InsertOrReplace(editorEntity);
            if (userId != output.AuthorUserId)
            {
                batchOperation.InsertOrReplace(authorEntity);
            }
            var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
            await table.ExecuteBatchAsync(batchOperation);

            var startTime = DateTime.SpecifyKind(output.StartTime, DateTimeKind.Utc);

            // SignalR
            this.GetAuthorConnections(partitionKey).ReviewStarted(reviewId, startTime, output.ReviewerUserId, output.ReviewerName);

            return(StatusCode(HttpStatusCode.NoContent));
        }
Ejemplo n.º 6
0
        public async Task <IHttpActionResult> GetAllReviewPieces(int exerciseId)
        {
            var filterPartition = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, ReviewPiece.GetPartitionKey(exerciseId));
            var query           = new TableQuery <ReviewPiece>().Where(filterPartition);
            var allPieces       = await AzureStorageUtils.ExecuteQueryAsync(AzureStorageUtils.TableNames.ReviewPieces, query);

            // Enforce access rights. The exercise author cannot see review items in an unfinished review. An access entity is written when a review is finished. See ReviewsApiController.PostFinishReview
            var userAccessCode = ReviewPiece.PieceTypes.Viewer + KeyUtils.IntToKey(this.GetUserId());
            // Find the ReviewIds which are allowed to access.
            var reviewIds = allPieces
                            .Where(i => ReviewPiece.GetUserAccessCode(i.RowKey) == userAccessCode)
                            .Select(i => ReviewPiece.GetReviewId(i.RowKey))
                            .ToList();

            RemoveAccessEntries(allPieces);

            // Filter the record set.
            var accessablePieces = allPieces.Where(i => reviewIds.Contains(ReviewPiece.GetReviewId(i.RowKey)));
            var piecesArr        = accessablePieces.Select(i => i.Json).ToArray();

            return(Ok(piecesArr));
        }
Ejemplo n.º 7
0
        public async Task <IHttpActionResult> DeletePiece(string partitionKey, string rowKey)
        {
            // Check access rights.
            var reviewId     = ReviewPiece.GetReviewId(rowKey);
            var table        = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
            var userIsEditor = await UserIsEditor(partitionKey, reviewId, table);

            if (userIsEditor)
            {
                var entity = new ReviewPiece()
                {
                    PartitionKey = partitionKey,
                    RowKey       = rowKey,
                    ETag         = "*",
                };
                var deleteOperation = TableOperation.Delete(entity);
                // If the piece is not yet saved, we will get "The remote server returned an error: (404) Not Found."
                // The approved solution from MS is to try to retrieve the entity first. Catching exception is a hack which is appropriate for a single entity, it is not compatible with a Batch operation.
                try
                {
                    await table.ExecuteAsync(deleteOperation);
                }
                catch (StorageException ex)
                {
                    if (ex.RequestInformation.HttpStatusCode != (int)HttpStatusCode.NotFound)
                    {
                        throw;
                    }
                }

                // Notify the exercise author in real-time.
                var pieceType = ReviewPiece.GetType(rowKey);
                var pieceId   = ReviewPiece.GetPieceId(rowKey);
                this.GetAuthorConnections(partitionKey).PieceDeleted(reviewId, pieceType, pieceId);
            }

            return(StatusCode(userIsEditor ? HttpStatusCode.NoContent : HttpStatusCode.BadRequest));
        }
Ejemplo n.º 8
0
        public async Task <IHttpActionResult> PutReviewPieces([FromBody] IEnumerable <ReviewPiece> pieces)
        {
            // All pieces must belong to the same exercise and review.
            var partitionKey = pieces
                               .Select(i => i.PartitionKey)
                               .GroupBy(i => i)
                               .Select(i => i.Key)
                               .Single();

            var reviewId = pieces
                           .Select(i => ReviewPiece.GetReviewId(i.RowKey))
                           .GroupBy(i => i)
                           .Select(i => i.Key)
                           .Single();

            // Ensure that the user is the actual reviewer. Check the presense of the access entry for the user. All pieces must belong to the same exercise and review.
            var table        = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
            var userIsEditor = await UserIsEditor(partitionKey, reviewId, table);

            if (userIsEditor)
            {
                var batchOperation = new TableBatchOperation();
                foreach (var piece in pieces)
                {
                    batchOperation.InsertOrReplace(piece);
                }
                await table.ExecuteBatchAsync(batchOperation);
            }

            // Notify the exercise author in real-time.
            var piecesArr = pieces.Select(i => i.Json).ToArray();

            this.GetAuthorConnections(partitionKey).PiecesChanged(piecesArr);

            return(StatusCode(userIsEditor ? HttpStatusCode.NoContent : HttpStatusCode.BadRequest));
        }
Ejemplo n.º 9
0
        public async Task <ActionResult> Upload(HttpPostedFileBase mp3File, HttpPostedFileBase xmlFile,
                                                string userdir = "", string title = null, string learnerSkype = null)
        {
            int exerciseId = 0;
            int newUserId  = 0;

            var userId = this.GetUserId();

            // Save the recording
            if (mp3File != null)
            {
                ViewBag.mp3FileName = mp3File.FileName;
                // We can receive garbage from the wild web.
                try
                {
                    using (var stream = mp3File.InputStream)
                    {
                        var duration = RecordingUtils.GetMp3Duration(stream);
                        if (duration > 0)
                        {
                            var recordingTitle = !String.IsNullOrEmpty(title)
                                ? title
                                : Path.GetFileNameWithoutExtension(mp3File.FileName);

                            exerciseId = await ExerciseUtils.SaveRecording(
                                stream,
                                userId,
                                ArtifactType.Mp3,
                                ServiceType.IeltsSpeaking,
                                duration,
                                recordingTitle
                                );
                        }
                    }
                }
                catch
                {
                }
            }

            // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided.
            if (exerciseId != 0 && this.GetUserIsTeacher())
            {
                // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous.
                // Continue anyway with either old or new user.
                if (!String.IsNullOrEmpty(learnerSkype))
                {
                    newUserId = (await DapperHelper.QueryResilientlyAsync <int?>("dbo.exeTryChangeExerciseAuthor",
                                                                                 new
                    {
                        ExerciseId = exerciseId,
                        UserId = userId,
                        SkypeName = learnerSkype,
                    },
                                                                                 CommandType.StoredProcedure))
                                .SingleOrDefault()
                                .GetValueOrDefault();
                }

                IEnumerable <int> remarkSpots = new int[] { };

                // If there is a remark spot collection XML file produced by the teacher coming with the recording.
                if (xmlFile != null)
                {
                    // We can receive garbage from the wild web.
                    try
                    {
                        XElement root;
                        using (var stream = xmlFile.InputStream)
                        {
                            root = XElement.Load(stream);
                        }

                        var startedText = root.Attribute("time").Value;
                        var started     = DateTime.Parse(startedText, null, DateTimeStyles.RoundtripKind);

                        remarkSpots = root
                                      .Elements("RemarkSpot")
                                      .Select(i =>
                        {
                            var spot = DateTime.Parse(i.Attribute("time").Value, null, DateTimeStyles.RoundtripKind);
                            return(Convert.ToInt32((spot - started).TotalMilliseconds));
                        })
                                      .ToList();
                    }
                    catch
                    {
                    }
                }

                // We have got remark spots. Create a review.
                if (remarkSpots.Any())
                {
                    var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new
                    {
                        ExerciseId = exerciseId,
                        UserId = userId,
                    },
                                                                                   CommandType.StoredProcedure
                                                                                   ))
                                   .SingleOrDefault();

                    // Save remarks.
                    if (reviewId != 0)
                    {
                        var pieces = remarkSpots
                                     .OrderBy(i => i)
                                     .Distinct()
                                     .Select((i, index) =>
                        {
                            var finish = Math.Max(0, i - 1000);      // We allow for a 1 second delay between the actual learner's mistake and the teacher's action.
                            var start  = Math.Max(0, finish - 2000); // Assume the spot is 2 second long.

                            var remark = new RemarkSpot
                            {
                                ReviewId = reviewId,
                                Type     = ReviewPiece.PieceTypes.Remark,
                                Id       = index,
                                Start    = start,
                                Finish   = finish,
                            };

                            return(new ReviewPiece
                            {
                                PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                                RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index),
                                Json = JsonUtils.SerializeAsJson(remark),
                            });
                        });

                        var batchOperation = new TableBatchOperation();
                        foreach (var piece in pieces)
                        {
                            batchOperation.InsertOrReplace(piece);
                        }

                        // Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities.
                        // The write entity will be deleted on review finish.
                        var viewerEntity = new ReviewPiece()
                        {
                            PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                            RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId),
                        };
                        batchOperation.InsertOrReplace(viewerEntity);

                        var editorEntity = new ReviewPiece()
                        {
                            PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                            RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId),
                        };
                        batchOperation.InsertOrReplace(editorEntity);

                        var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
                        await table.ExecuteBatchAsync(batchOperation);

                        // Redirect to the reviews page.
                        return(RedirectToAction("Edit", "Reviews", new { id = reviewId.ToString() }));
                    }
                } // end of if (remarkSpots.Any())
            }     // end of if (exerciseId && this.GetIsTeacher())

            //
            if (exerciseId != 0)
            {
                return(newUserId == 0 ? RedirectToAction("Index") : RedirectToAction("Index", "Reviews"));
            }

            ViewBag.Success = exerciseId != 0;
            ViewBag.UserDir = userdir;

            return(View());
        } // end of Upload()
        public async Task <IHttpActionResult> PutSave([FromUri] string uuid, [FromBody] JObject values)
        {
            string redirectToUrl = null;

            var learnerSkype = (string)values["learnerSkype"];
            var titleValue   = (string)values["title"];
            var comment      = (string)values["comment"];
            var remarkSpots  = ((JArray)values["remarkSpots"]).Select(i => (int)i); // milliseconds

            var userId = this.GetUserId();

            //-- Call the remote transcoding service.
            var                 host      = ConfigurationManager.AppSettings["RecorderHost"];
            var                 urlFormat = "http://{0}/api/recordings/from_freeswitch/?uuid={1}&userId={2}";
            var                 url       = String.Format(urlFormat, host, uuid, userId);
            HttpClient          client    = new HttpClient();
            HttpResponseMessage response  = await client.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                throw new HttpException("from_freeswitch");
            }
            // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'RecordingInfo' from content with media type 'text/html'.
            var recordingDetails = await response.Content.ReadAsAsync <RecordingDetails>();

            // Make sure the duration is known. If the transcoder has failed to parse the ffmpeg logs, it returns DurationMsec = 0.
            if (recordingDetails.TotalDuration == 0)
            {
                // Read the blob and try to determine the duration directly.
                recordingDetails.TotalDuration =
                    await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Artifacts, recordingDetails.BlobName);
            }

            var title = !String.IsNullOrEmpty(titleValue)
                ? titleValue
                : Path.GetFileNameWithoutExtension(recordingDetails.BlobName);

            //-- Create a database record for the exercise.
            var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId,
                                                                ServiceType.IeltsSpeaking, ArtifactType.Mp3, recordingDetails.TotalDuration, title, null, comment);

            // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided.
            if (exerciseId != 0 && this.GetUserIsTeacher())
            {
                //-- Change the exercise author.
                // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous.
                // Continue anyway with either old or new user.
                var newUserId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeTryChangeExerciseAuthor",
                                                                                new
                {
                    ExerciseId = exerciseId,
                    UserId = userId,
                    SkypeName = learnerSkype,
                },
                                                                                CommandType.StoredProcedure))
                                .SingleOrDefault();

                //-- Create a review. We do not charge the learner for a review initiated by the teacher.
                var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview",
                                                                               new
                {
                    ExerciseId = exerciseId,
                    UserId = userId,
                },
                                                                               CommandType.StoredProcedure))
                               .SingleOrDefault();

                //-- We have got remark spots. Save remarks.
                if (reviewId != 0 && remarkSpots.Any())
                {
                    var pieces = remarkSpots
                                 .OrderBy(i => i)
                                 .Distinct()
                                 .Select((i, index) =>
                    {
                        var finish = Math.Max(0, i - AssumedTeacherReactionDelayMSec);
                        var start  = Math.Max(0, finish - DefaultSpotLengthMSec);

                        var remark = new RemarkSpot
                        {
                            ReviewId = reviewId,
                            Type     = ReviewPiece.PieceTypes.Remark,
                            Id       = index,
                            Start    = start,
                            Finish   = finish,
                        };

                        return(new ReviewPiece
                        {
                            // This PartitionKey calculation is redundunt, but the overhead is neglectable.
                            PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                            RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index),
                            Json = JsonUtils.SerializeAsJson(remark),
                        });
                    });

                    var batchOperation = new TableBatchOperation();
                    foreach (var piece in pieces)
                    {
                        batchOperation.InsertOrReplace(piece);
                    }

                    //-- Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities.
                    // The write entity will be deleted on review finish.
                    var viewerEntity = new ReviewPiece()
                    {
                        PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                        RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId),
                    };
                    batchOperation.InsertOrReplace(viewerEntity);

                    var editorEntity = new ReviewPiece()
                    {
                        PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                        RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId),
                    };
                    batchOperation.InsertOrReplace(editorEntity);

                    var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
                    await table.ExecuteBatchAsync(batchOperation);
                } // end of if (reviewId != 0 && remarkSpots.Any())

                //-- Return the link to the review page. The client will redirect.
                redirectToUrl = Url.Link("Default", new
                {
                    Controller = "Reviews",
                    Action     = reviewId != 0 ? "Edit" : "Index",
                    id         = reviewId.ToString()
                });
            } // end of if (exerciseId && this.GetIsTeacher())

            return(Created(redirectToUrl, redirectToUrl));
        }