Exemple #1
0
 def load(self, instance, xblock):
     """
     Get the filesystem for the field specified in 'instance' and the xblock in 'xblock'
     It is locally scoped.
     """
     # TODO: Get xblock from context, once the plumbing is piped through
     return djpyfs.get_filesystem(scope_key(instance, xblock))
Exemple #2
0
 def load(self, instance, xblock):
     """
     Get the filesystem for the field specified in 'instance' and the xblock in 'xblock'
     It is locally scoped.
     """
     # TODO: Get xblock from context, once the plumbing is piped through
     return djpyfs.get_filesystem(scope_key(instance, xblock))
Exemple #3
0
def index(request):
    fs = djpyfs.get_filesystem("sample")
    f = fs.open("uparrow.png", "wb")
    png.Writer(len(arrow[0]), len(arrow), greyscale = True, bitdepth = 1).write(f, arrow)
    f.close()

    url = fs.get_url("uparrow.png")

    return HttpResponse("<html><body>Hello, world. You're at the polls index. <img src=\"{source}\"> </body></html>".format(source=url))
Exemple #4
0
    def load(self, instance, xblock):
        """
        Get the filesystem for the field specified in 'instance' and the
        xblock in 'xblock' It is locally scoped.
        """

        # TODO: Get xblock from context, once the plumbing is piped through
        if djpyfs:
            return djpyfs.get_filesystem(scope_key(instance, xblock))
        else:
            # The reference implementation relies on djpyfs
            # https://github.com/edx/django-pyfs
            # For Django runtimes, you may use this reference
            # implementation. Otherwise, you will need to
            # patch pyfilesystem yourself to implement get_url.
            raise NotImplementedError("djpyfs not available")
class RichReviewXBlock(XBlock):
    """
    This XBlock
        (1) serves the RichReview web app at '../webapps/app_richreview', and
        (2) manages backend data flow of the app.
    """

    # This filesystem stores the document (.PDF), the document metadata (.js), and audio (.WAV) files.
    # The audio files will be stored at '<xblockid>/<groupid>/'.
    # The document files will be stored at '<xblockid>/<docs>/'.
    # Note that the PDF file will be renamed to the file contents' SHA1 hash value
    #   (e.g., '6adfb183a4a2c94a2f92dab5ade762a47889a5a1.pdf').
    fs = djpyfs.get_filesystem("rrfs")
    # The RichReview web app will be served via this static URL.
    # The web app files are stored in '/static/app', and will be maintained or updated through a github repository.
    if hasattr(settings, 'DJFS'):
        djfs_settings = settings.DJFS
    else:
        djfs_settings = {
            'type': 'osfs',
            'directory_root': 'common/static/djpyfs',
            'url_root': '/static/djpyfs'
        }

    # SHA1 hash of the PDF file under discussion.
    # This value will be updated when a new file is uploaded from the Studio view.
    discussion_docid = String(help="Id of the pdf under discussion",
                              default="",
                              scope=Scope.settings)

    # This dictionary stores the list of student's anonymous ids for a given group.
    # Usage: students_of_group[<groupid>] == [<student id 0>, <student id 1>, ...]
    #        Use self.assign_group() to update
    students_of_group = Dict(help="groupid of this specific student",
                             default={},
                             scope=Scope.user_state_summary)

    # This dictionary maps each student to a group in parallel with students_of_group
    # Usage: Use self.assign_group() to update
    group_of_student = Dict(help="list of students in each group",
                            default={},
                            scope=Scope.user_state_summary)

    # This dictionary maps each groups to the id of the document (.PDF) that they are supposed to discuss
    # Usage: pdf_of_group[<groupid>] == <pdfid>
    doc_of_group = Dict(help="Id of the pdf assigned to each group",
                        default={},
                        scope=Scope.user_state_summary)

    #This dictionary stores the list of comments & commands for each group
    # Usage: cmds_of_group[<groupid>] == [<cmd0>, <cmd1>, ... ]
    cmds_of_group = Dict(
        help="list of annotation data (JSON format) in a chronological order",
        default={},
        scope=Scope.user_state_summary)

    @property
    def course_id(self):
        try:
            return str(self.runtime.course_id)
        except:
            return 'testcourseid000'

    @property
    def xblock_id(self):
        """
        ID of this xblock is useful for defining file storage path. For example,
        if 'the Textbook's Chapter 12' is the discussion topic, all the commentary data will be
         stored in the form of <course_id>/<** xblock_id **>/<groupid>/<someid>.wav
        """
        return self.scope_ids.usage_id.block_id

    @property
    def anonymous_student_id(self):
        try:
            return self.runtime.anonymous_student_id
        except:
            return 'anonymouseuserid0000'

    @property
    def user_is_staff(self):
        """
        Staffs will intervene into the on-going discussion using their XBlock's ** live view **.
         We will identify them with this flag. Note that all the commenting--including staffs'--
         will be done in the student view, not studio view.
        """
        try:
            return self.runtime.user_is_staff
        except:
            return False

    @property
    def group_id(self):
        """
        Get current user's group_id
        """
        if not self.anonymous_student_id in self.group_of_student:
            self.assign_group()
        return self.group_of_student[self.anonymous_student_id]

    @property
    def xblock_path(self):
        return self.course_id + "/" + self.xblock_id

    @property
    def pdf_path(self):
        """
        Path where the PDF file will be stored.
        """
        return "doc/" + self.discussion_docid + ".pdf"

    @property
    def pdfjs_path(self):
        """
        Path where the PDF's layout data file (in Json) will be stored.
        """
        return "doc/" + self.discussion_docid + ".js"

    @property
    def is_pdf_ready(self):
        """
        self.discussion_docid is set to SHA1 of the PDF file ** after ** the staff upload the file.
        """
        return self.discussion_docid != "" and self.fs.exists(
            self.pdf_path) and self.fs.exists(self.pdfjs_path)

    @property
    def richreview_app_urls(self):
        return self.get_webapp_urls('public/webapps/richreview',
                                    exclude=r'(\..*)|(^test\/.*)')

    @property
    def multicolumn_app_urls(self):
        return self.get_webapp_urls('public/webapps/multicolumn',
                                    exclude=r'\..*')

    def get_webapp_urls(self, path, exclude):
        files = get_local_resource_list('/' + path, exclude)
        urls = {}
        for file in files:
            urls[file] = self.runtime.local_resource_url(
                self, path + '/' + file)
        return urls

    def get_audio_filepath(self, groupid, audio_filename):
        return self.xblock_path + "/" + groupid + "/" + audio_filename + ".wav"

    def get_available_group(self):
        """
        Returns any available group that have less than 5 members. If all groups are full, then
        create a group and returns.
        """
        for groupid in self.students_of_group:
            if len(self.students_of_group[groupid]
                   ) < 5:  #hardcoded maximum number of users in a group
                return groupid
        new_groupid = uuid4().hex
        self.students_of_group[new_groupid] = []
        self.doc_of_group[new_groupid] = self.discussion_docid
        return new_groupid

    def assign_group(self):
        """
        Add this student to an any available group. If the group is not exist, create it, and add the student.
        """
        groupid = self.get_available_group()
        self.students_of_group[groupid].append(self.anonymous_student_id)
        self.group_of_student[self.anonymous_student_id] = groupid

    def get_richreview_user(self, anonymous_student_id):
        """
        Returns a json representation of RichReview user profile
        """
        try:
            real_user = self.runtime.get_real_user(anonymous_student_id)
            return {
                "id": anonymous_student_id,
                "nick": real_user.username,
                "email": real_user.email
            }
        except:
            return {
                "id": anonymous_student_id,
                "nick": "Student Name",
                "email": "*****@*****.**"
            }

    def init_cmds(self, groupid):
        """
        Creat a new cmd slot for the group, if it is not there.
        """
        if not groupid in self.cmds_of_group:
            self.cmds_of_group[groupid] = []

    def add_cmd(self, cmd, groupid):
        self.init_cmds(groupid)
        self.cmds_of_group[groupid].append(json.loads(cmd))

    def get_cmds(self, groupid, cmds_downloaded_n):
        """
        Get new cmds that has been updated after the last download.
        """
        self.init_cmds(groupid)
        cmds = self.cmds_of_group[groupid][int(cmds_downloaded_n):]
        for idx, cmd in enumerate(cmds):
            if cmd["op"] == "CreateComment" and cmd["type"] == "CommentAudio":
                cmds[idx]["data"]["audiofileurl"] = self.fs.get_url(
                    self.get_audio_filepath(
                        groupid, cmd["user"] + "_" + cmd["data"]["aid"]),
                    RESOURCE_EXPIRATION_TIME)
        cmd_str = []
        for cmd in cmds:
            cmd_str.append(json.dumps(cmd))
        return cmd_str

    def student_view(self, context=None):
        """
        The primary view of the RichReviewXBlock, shown to students
        when viewing courses.
        """
        frag = Fragment()
        frag.add_css(load_resource("static/css/richreview.css"))
        frag.add_javascript(
            load_resource("static/js/src/richreview_student.js"))
        frag.add_content(
            render_template(
                "templates/richreview_student.html", {
                    'xblock_id': self.xblock_id,
                    'student_id': self.anonymous_student_id,
                    'user_is_staff': str(self.user_is_staff),
                    'students_of_group': str(self.students_of_group),
                    'group_of_student': str(self.group_of_student),
                    'discussion_docid': self.discussion_docid,
                    'is_debug': False,
                    'is_pdf_ready': self.is_pdf_ready
                }))

        frag.initialize_js('RichReviewXBlock')
        return frag

    def studio_view(self, context=None):
        """
        The staff's view of RichReviewXBlock. Staffs can upload discussion topic PDF or view the students'
        discussion activities
        """
        frag = Fragment()
        frag.add_css(load_resource("static/css/richreview.css"))
        frag.add_javascript(
            load_resource("static/js/src/richreview_studio.js"))
        frag.add_content(
            render_template(
                "templates/richreview_studio.html", {
                    'xblock_id':
                    self.xblock_id,
                    'student_id':
                    self.anonymous_student_id,
                    'user_is_staff':
                    str(self.user_is_staff),
                    'students_of_group':
                    str(self.students_of_group),
                    'group_of_student':
                    str(self.group_of_student),
                    'discussion_docid':
                    self.discussion_docid,
                    'is_pdf_ready':
                    self.is_pdf_ready,
                    'is_debug':
                    False,
                    'pdf_url':
                    self.fs.get_url(self.pdf_path, RESOURCE_EXPIRATION_TIME),
                    'pdfjs_url':
                    self.fs.get_url(self.pdfjs_path, RESOURCE_EXPIRATION_TIME),
                    'loader_gif_url':
                    self.runtime.local_resource_url(self,
                                                    "public/ajax-loader.gif")
                }))
        frag.initialize_js('RichReviewXBlockStudio', "abcd")
        return frag

    def get_mupla(self):
        tmp_path = "/tmp/" + uuid4().hex + ".pdf"
        with self.fs.open(self.pdf_path, "rb") as src:
            with open(tmp_path, "wb") as dst:
                dst.write(src.read())
                dst.close()
                js = mupla_ctype.PyMuPlaRun(tmp_path)
            src.close()
        return js

    @XBlock.handler
    def pdfupload(self, request, suffix=''):
        """
        Handles PDF upload request from the studio
        """
        try:
            temp_path = self.xblock_path + '/' + uuid4().hex + '.pdf'
            osfs_save_formfile(self.fs, temp_path,
                               request.POST['pdffile'].file)
            with self.fs.open(temp_path, "rb") as f:
                h = hashlib.sha1()
                h.update(f.read())
                self.discussion_docid = h.hexdigest()
                osfs_copy_file(self.fs, temp_path, self.pdf_path)

                mupla_pdfs_folder_path = self.xblock_path + "/" + self.discussion_docid
                mupla_pdf_path = mupla_pdfs_folder_path + "/merged.pdf"
                osfs_mkdir(self.fs, mupla_pdf_path)
                osfs_copy_file(self.fs, self.pdf_path, mupla_pdf_path)

                with self.fs.open(mupla_pdfs_folder_path + "/merged.js",
                                  "wb") as f_js:
                    json.dump(self.get_mupla(), f_js)
                    f_js.close()
                f.close()

            return Response(
                json_body={
                    "pdf_url":
                    self.fs.get_url(mupla_pdfs_folder_path +
                                    "/merged.pdf", RESOURCE_EXPIRATION_TIME),
                    "js_url":
                    self.fs.get_url(mupla_pdfs_folder_path +
                                    "/merged.js", RESOURCE_EXPIRATION_TIME),
                    "multicolumn_webapp_urls":
                    self.multicolumn_app_urls,
                })
        except Exception:
            print(traceback.format_exc())
            return Response(status=406)

    @XBlock.json_handler
    def pdfjsupload(self, data, suffix=""):
        """
        Receives the analyzed layout data from the MultiColumn analysis engine.
        """
        with self.fs.open(self.pdfjs_path, "wb") as f:
            json.dump(data, f)
            f.close()
        return {"response": 'ok'}

    @XBlock.json_handler
    def pdfdelete(self, request, suffix=''):
        """
        Handles PDF delete request from the studio
        """
        if self.fs.exists(self.pdf_path):
            self.fs.remove(self.pdf_path)
        if self.fs.exists(self.pdfjs_path):
            self.fs.remove(self.pdfjs_path)
        self.discussion_docid = ""
        return {"response": 'ok'}

    @XBlock.json_handler
    def get_richreview_context(self, req, suffix=''):
        """
        Serves RichReview web app contexts
        """
        return {
            "pdfid": self.discussion_docid,
            "docid": self.xblock_id,
            "groupid": self.group_id,
            "app_urls": self.richreview_app_urls,
            "pdf_url": self.fs.get_url(self.pdf_path,
                                       RESOURCE_EXPIRATION_TIME),
            "pdfjs_url": self.fs.get_url(self.pdfjs_path,
                                         RESOURCE_EXPIRATION_TIME)
        }

    @XBlock.handler
    def serve_dbs(self, req, suffix=''):
        """
        Simple switch for the database service
        """
        return self.serve_dbs_map[req.GET["op"]](self, req.POST)

    @XBlock.handler
    def upload_audio(self, request, suffix=''):
        """
        Get audio file from the web app and stores it in the file system.
        POST['data'] is base64 encoded .WAV file stream. POST['fname'] is the filename in the form of <student_id>_<comment_id>.
        For example, 'b7fa14f2df91760225ec682fea1f9da9_2015-06-11T14:21:30.617Z'. Note that the <comment_id> is the time that
        the annotation was created.
        """
        datastr = request.POST['data']
        audio_data = datastr[datastr.index(',') + 1:]

        audio_filepath = self.get_audio_filepath(self.group_id,
                                                 request.POST['fname'])
        osfs_mkdir(self.fs, audio_filepath)

        with self.fs.open(audio_filepath, "wb") as f:
            f.write(audio_data.decode('base64'))
            f.close()
        return Response()

    def get_users_data(self, groupid):

        js = {}
        js["self"] = self.get_cur_user_json()
        js["users"] = self.get_group_users_json(groupid)
        return js

    def serve_dbs_user_data(self, post):
        """
        Serves user data. Note that js["self"] is always this user.
        js["self"] may not be in js["users"], but then the web app runs in an observer mode.
        """
        groupid = post["groupid"][4:]
        return Response(json_body=self.get_users_data(groupid))

    def serve_dbs_doc_groups(self, post):
        """
        Serves a list of all the groups in this xblock.
        """
        js = {
            "creationTime": "0",
            "name": "RichReview Discussion Group",
            "userid_n": "staff",
            "pdfid": self.discussion_docid,
            "groups": map(lambda x: 'grp:' + x, self.students_of_group.keys()),
            "id": "doc:" + self.xblock_id,
            "creationTimeStr": "Sat, 01/01/2000 00:00 AM"
        }
        return Response(json_body=js)

    def serve_dbs_log(self, post):
        """
        Logs user activity
        """
        js = {}
        return Response(json_body=js)

    def serve_dbs_download_cmds(self, post):
        """
        Serves updated cmds of this group
        """
        users = self.get_group_users_json(post["groupid_n"])
        update_user = int(post["cur_members_n"]) != len(users)
        js = {
            "cmds": self.get_cmds(post["groupid_n"],
                                  post["cmds_downloaded_n"]),
            "users":
            self.get_users_data(post["groupid_n"]) if update_user else "",
        }
        return Response(json_body=js)

    def serve_dbs_upload_cmd(self, post):
        """
        Uploads a new discussion to this group's data
        """
        self.add_cmd(post["cmd"], post["groupid_n"])
        return Response()

    def get_cur_user_json(self):
        return self.get_richreview_user(self.anonymous_student_id)

    def get_group_users_json(self, groupid):
        """
        Get list of users (in a RichReview's json form) for a given group
        """
        group_users = []
        if groupid in self.students_of_group:
            for userid in self.students_of_group[groupid]:
                group_users.append(self.get_richreview_user(userid))
        return group_users

    # switch for a database service
    serve_dbs_map = {
        "GetGroupData": serve_dbs_user_data,
        "GetDocGroups": serve_dbs_doc_groups,
        "WebAppLog": serve_dbs_log,
        "DownloadCmds": serve_dbs_download_cmds,
        "UploadCmd": serve_dbs_upload_cmd,
    }

    # workbench scenario
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("RichReviewXBlock", """
                <richreview/>
             """),
        ]