This is an automated email from the ASF dual-hosted git repository. brondsem pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/allura.git
commit 096aaa843a2886b6af201be51b4848fa1a6ef81c Author: Sanskriti Mohapatra <[email protected]> AuthorDate: Thu Nov 19 19:55:54 2020 +0530 [#8368] Snippets for the new Files App contribution --- Allura/allura/nf/allura/css/site_style.css | 9 + Allura/allura/templates/jinja_master/master.html | 19 + Allura/allura/templates/jinja_master/nav_menu.html | 3 + ForgeFiles/forgefiles/__init__.py | 16 + ForgeFiles/forgefiles/files_main.py | 660 +++++++++++++++++++++ ForgeFiles/forgefiles/model/__init__.py | 18 + ForgeFiles/forgefiles/model/files.py | 247 ++++++++ ForgeFiles/forgefiles/nf/files/css/files.css | 86 +++ ForgeFiles/forgefiles/nf/files/js/create_folder.js | 77 +++ ForgeFiles/forgefiles/nf/files/js/delete.js | 33 ++ ForgeFiles/forgefiles/nf/files/js/edit.js | 82 +++ ForgeFiles/forgefiles/nf/files/js/files.js | 94 +++ .../forgefiles/nf/files/js/publish_folder.js | 46 ++ ForgeFiles/forgefiles/nf/files/js/upload_files.js | 36 ++ ForgeFiles/forgefiles/templates/create_folder.html | 58 ++ ForgeFiles/forgefiles/templates/delete.html | 62 ++ ForgeFiles/forgefiles/templates/edit.html | 78 +++ ForgeFiles/forgefiles/templates/files.html | 193 ++++++ ForgeFiles/forgefiles/templates/mail.html | 34 ++ .../forgefiles/templates/publish_folder.html | 69 +++ ForgeFiles/forgefiles/templates/upload_file.html | 55 ++ ForgeFiles/forgefiles/tests/__init__.py | 17 + ForgeFiles/forgefiles/tests/functional/__init__.py | 17 + .../forgefiles/tests/functional/test_root.py | 144 +++++ ForgeFiles/forgefiles/tests/model/__init__.py | 51 ++ ForgeFiles/forgefiles/tests/model/test_files.py | 54 ++ ForgeFiles/forgefiles/tests/test_files_roles.py | 53 ++ ForgeFiles/setup.py | 32 + 28 files changed, 2343 insertions(+) diff --git a/Allura/allura/nf/allura/css/site_style.css b/Allura/allura/nf/allura/css/site_style.css index 77e4c7c..1f8b4d0 100644 --- a/Allura/allura/nf/allura/css/site_style.css +++ b/Allura/allura/nf/allura/css/site_style.css @@ -4082,3 +4082,12 @@ Nav bar styles moved out of navbar.css since they are more stylistic than layout .user-card .card-right .subitem a{ color: #00b3ff; } + +/* Download button style for Files plugin*/ +#download_button{ + float:right !important; + cursor: pointer !important; + height: 3em !important; + color:#0077aa !important; + display: none; +} diff --git a/Allura/allura/templates/jinja_master/master.html b/Allura/allura/templates/jinja_master/master.html index 3892f64..101ee23 100644 --- a/Allura/allura/templates/jinja_master/master.html +++ b/Allura/allura/templates/jinja_master/master.html @@ -45,6 +45,7 @@ {% do g.resource_manager.register_widgets(c) %} + {# paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ #} <!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]--> <!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]--> @@ -199,6 +200,24 @@ }); }); + +$(document).ready(function(){ + $.ajax({ + url:'/p/{{c.project.shortname}}/files/project_file/', + type:'GET', + success: function(res){ + var count = parseInt(res); + if (count != 0){ + $('#download_button').show(); + } + else{ + $('#download_button').hide(); + } + } +}); + +}); + </script> </body> </html> diff --git a/Allura/allura/templates/jinja_master/nav_menu.html b/Allura/allura/templates/jinja_master/nav_menu.html index 3904896..4b8ac26 100644 --- a/Allura/allura/templates/jinja_master/nav_menu.html +++ b/Allura/allura/templates/jinja_master/nav_menu.html @@ -40,6 +40,9 @@ {%- endif -%} </a> </h1> + <a href="/p/{{c.project.shortname}}/files/download_file?app_url={{c.app.url}}" > + <button class="btn-home btn-danger" id="download_button" >Download</button> + </a> {% set status = c.project.troves_by_type('developmentstatus')|sort(attribute='fullname') %} {% set status = status[-1] %} {% if status and status.shortname not in ['production', 'mature'] %} diff --git a/ForgeFiles/forgefiles/__init__.py b/ForgeFiles/forgefiles/__init__.py new file mode 100755 index 0000000..144e298 --- /dev/null +++ b/ForgeFiles/forgefiles/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/ForgeFiles/forgefiles/files_main.py b/ForgeFiles/forgefiles/files_main.py new file mode 100755 index 0000000..508e6f6 --- /dev/null +++ b/ForgeFiles/forgefiles/files_main.py @@ -0,0 +1,660 @@ +'''This is the main controller module for the Files Plugin.''' + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +# !/bin/python + +import logging +from urllib import unquote + +from tg import config, redirect, expose, flash +from tg.decorators import with_trailing_slash, without_trailing_slash +from tg import tmpl_context as c, app_globals as g +from tg import request, response +from jinja2.exceptions import TemplateNotFound + +from allura.app import Application +from allura.controllers import BaseController +from allura.lib.decorators import require_post +from allura.lib.widgets.subscriptions import SubscribeForm +from allura import model as M +from allura.controllers import attachments as att +from allura import version +from allura.model.timeline import TransientActor + + +from bson import ObjectId +from webob import exc + +# local imports ## +from forgefiles.model.files import UploadFolder, UploadFiles, Upload + +log = logging.getLogger(__name__) + + +class FilesApp(Application): + """Files plugin for the Allura platform""" + + __version__ = version.__version__ + tool_label = 'Files' + tool_description = """Upload executables for your project. + You may maintain version specific executables as well.""" + default_mount_label = 'Files' + default_mount_point = 'files' + uninstallable = True + ordinal = 9 + max_instances = 1 + + def __init__(self, project, config): + Application.__init__(self, project, config) + self.root = FilesController() + + def install(self, project): + 'Set up any default permissions and roles here' + self.config.options['project_name'] = project.name + super(FilesApp, self).install(project) + role_anon = M.ProjectRole.by_name('*anonymous')._id + self.config.acl = [ + M.ACE.allow(role_anon, 'read'), + ] + + def uninstall(self, project): + "Remove all the tool's artifacts from the database" + app_config_id = {'app_config_id': c.app.config._id} + Upload.query.remove(app_config_id) + UploadFolder.query.remove(app_config_id) + file_objects = UploadFiles.query.find(app_config_id).all() + for file_object in file_objects: + file_object.delete() + super(FilesApp, self).uninstall(project) + + +def get_parent_folders(linked_file_object=None): + + '''Returns the list of the parent folders for the current file or folder''' + + parent_folder = linked_file_object.parent_folder if linked_file_object else None + parent_folders_list = [] + while parent_folder: + parent_folders_list.append(str(parent_folder._id)) + parent_folder = parent_folder.parent_folder + parent_folders_list = list(set(parent_folders_list)) + return parent_folders_list + + +class FilesController(BaseController): + """Root controller for the Files Application""" + + @expose('jinja:forgefiles:templates/files.html') + def index(self): + + '''Index method for the Root controller''' + + folder_object = None + file_object = None + + upload_object = Upload.query.get(project_id=c.project._id) + self.attachment = AttachmentsController(upload_object) + file_objects = UploadFiles.query.find({'project_id': c.project._id, 'parent_folder_id': None}) + file_objects = file_objects.sort([('created_date', -1)]).all() + folder_objects = UploadFolder.query.find({'project_id': c.project._id, 'parent_folder_id': None}) + folder_objects = folder_objects.sort([('created_date', -1)]).all() + if c.user in c.project.admins(): + M.Mailbox.subscribe(type='direct') + c.subscribe_form = SubscribeForm(thing='files') + tool_subscribed = M.Mailbox.subscribed() + if tool_subscribed: + subscribed = M.Mailbox.subscribed() + else: + subscribed = False + file_object = UploadFiles.query.get(project_id=c.project._id, linked_to_download=True) + parents = get_parent_folders(linked_file_object=file_object) + + return dict(file_objects=file_objects, + folder_objects=folder_objects, folder_object=folder_object, file_object=file_object, + subscribed=subscribed, parents=parents) + + def get_parent_folder_url(self, parent_folder_id): + + ''' Returns the url,parent_folder and id of parent_folder object if object is there''' + + if (parent_folder_id == 'None') or (not parent_folder_id): + parent_folder_id = None + parent_folder = None + url = c.app.url + else: + parent_folder = UploadFolder.query.get(_id=ObjectId(parent_folder_id)) + parent_folder_id = ObjectId(parent_folder._id) + url = parent_folder.url() + return parent_folder_id, parent_folder, url + + @require_post() + @expose() + def create_folder(self, parent_folder_id=None, folder_name=None): + + '''Controller method for creating a folder. The folder is stored in UploadFolder collection''' + + parent_folder_id, parent_folder, url = self.get_parent_folder_url(parent_folder_id) + if folder_name: + folder_object = UploadFolder.query.find({ + 'project_id': c.project._id, 'folder_name': folder_name, + 'parent_folder_id': parent_folder_id}).first() + if folder_object: + flash('Folder with the same name already exists!') + else: + folder_object = UploadFolder(folder_name=folder_name) + folder_object.parent_folder_id = parent_folder_id + parent = parent_folder + while parent: + parent.folder_ids.append(str(folder_object._id)) + parent = parent.parent_folder + flash('Folder is created successfully') + g.director.create_activity(c.user, 'created', folder_object, related_nodes=[c.project]) + else: + flash('Folder is not created successfully') + return redirect(url) + + @require_post() + @expose() + def upload_file(self, parent_folder_id=None, file_upload=None, filename=None): + + '''Controller method for creating a folder. The folder is stored in UploadFolder collection''' + + parent_folder_id, parent_folder, url = self.get_parent_folder_url(parent_folder_id) + if file_upload is not None: + file_object = UploadFiles.query.find({ + 'project_id': c.project._id, 'filename': filename, + 'parent_folder_id': parent_folder_id}).first() + if file_object: + flash('File with the same name already exists!') + else: + upload_object = Upload( + project_id=c.project._id, filename=filename, filetype=file_upload.type) + attach_object = upload_object.attach( + filename, file_upload.file, parent_folder_id=parent_folder_id) + if attach_object.parent_folder: + upload_object.file_url = attach_object.parent_folder.url() + else: + upload_object.file_url = c.app.url + parent = parent_folder + while parent: + parent.file_ids.append(str(attach_object._id)) + parent = parent.parent_folder + flash('File is uploaded successfully') + g.director.create_activity(c.user, 'uploaded', upload_object, related_nodes=[c.project]) + else: + flash('File is not uploaded successfully') + return redirect(url) + + @require_post() + @expose() + def delete_file(self, file_id=None): + + '''Controller method to delete a file''' + + file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + file_name = file_object.filename + transient_actor = TransientActor(activity_name=file_name) + url = c.app.url + if file_id is not None: + self.delete_file_from_db(file_id=file_id) + parent_folder = file_object.parent_folder + if parent_folder: + url = parent_folder.url() + flash('File is successfully deleted') + else: + flash('File is not deleted') + g.director.create_activity( + c.user, 'deleted the file', transient_actor, related_nodes=[c.project]) + M.main_orm_session.flush() + return redirect(url) + + def delete_file_from_db(self, file_id=None): + + '''Method to delete a file from db''' + + file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + Upload.query.remove({'_id': file_object.artifact_id}) + file_object.delete() + + def delete_folder_recursively(self, folder_id): + + '''This method is called recursively to delete folder in a hierarchy''' + + sub_file_objects = UploadFiles.query.find(dict({ + 'project_id': c.project._id, 'parent_folder_id': ObjectId(folder_id)})).all() + for file_object in sub_file_objects: + self.delete_file_from_db(file_id=file_object._id) + sub_folder_objects = UploadFolder.query.find({ + 'project_id': c.project._id, 'parent_folder_id': ObjectId(folder_id)}).all() + for folder_object in sub_folder_objects: + self.delete_folder_recursively(folder_object._id) + UploadFolder.query.remove(dict({'_id': ObjectId(folder_id)})) + + @without_trailing_slash + @require_post() + @expose('jinja:forgefiles:templates/files.html') + def delete_folder(self, folder_id=None): + + '''Controller method to delete a folder''' + + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + folder_name = folder_object.folder_name + transient_actor = TransientActor(activity_name=folder_name) + url = c.app.url + if folder_id is not None: + self.delete_folder_recursively(folder_id) + if folder_object.parent_folder: + url = folder_object.parent_folder.url() + flash('Folder is deleted Successfully') + else: + flash('Folder is not deleted') + g.director.create_activity( + c.user, 'deleted the folder', transient_actor, related_nodes=[c.project]) + M.main_orm_session.flush() + return redirect(url) + + @expose() + def link_file(self, file_id=None, status=None): + + '''Controller method to link a file to the download button''' + if status == 'False': + linkable_file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + linkable_file_object.linked_to_download = False + M.main_orm_session.flush() + else: + file_objects = UploadFiles.query.find({'project_id': c.project._id}).all() + for file_object in file_objects: + if file_object.linked_to_download: + file_object.linked_to_download = False + linkable_file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + linkable_file_object.linked_to_download = True + M.main_orm_session.flush() + + @expose() + def download_file(self, filename=None, app_url=None): + + '''Controller method to download a file''' + + if filename: + request_path = request.path.split(c.app.url)[-1].rstrip('/') + request_path = unquote(request_path) + linked_file_object = UploadFiles.query.find({ + 'project_id': c.project._id, 'filename': filename, 'path': request_path}).first() + upload_object = Upload.query.find({'_id': linked_file_object.artifact_id}).first() + else: + linked_file_object = UploadFiles.query.find({ + 'project_id': c.project._id, 'linked_to_download': True}).first() + if linked_file_object: + upload_object = Upload.query.find({'_id': linked_file_object.artifact_id}).first() + else: + upload_object = None + if linked_file_object: + try: + wrapper = upload_object.attachments[::-1] + data = wrapper + response.headers['Content-Type'] = 'application/octet' + response.headers['Content-Length'] = len(data) + response.headers['Pragma'] = 'public' + response.headers['Cache-Control'] = 'max-age=0' + requested_filename = linked_file_object.filename + response.headers['Content-Disposition'] = 'attachment; filename="%s"' % (requested_filename) + M.Mailbox.subscribe(type='direct') + return linked_file_object.serve(embed=True) + except Exception as e: + log.exception('%s error to download the file', e) + else: + data = 'No artifact available' + flash('No artifact available') + return redirect(app_url) + + @require_post() + @expose() + def edit_folder(self, folder_id=None, folder_name=None): + + '''Controller method to edit the folder name''' + + url = c.app.url + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + if folder_object: + folder_object.folder_name = folder_name + flash("Folder name edited successfully") + if folder_object.parent_folder: + url = folder_object.parent_folder.url() + else: + flash("Folder name not edited") + M.main_orm_session.flush() + redirect(url) + + @require_post() + @expose() + def edit_file(self, file_id=None, file_name=None): + + '''Controller method to edit the file name''' + + url = c.app.url + file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + upload_object = Upload.query.get(_id=file_object.artifact_id) + if file_object: + upload_object.filename = file_name + file_object.filename = file_name + flash("File name edited successfully") + if file_object.parent_folder: + url = file_object.parent_folder.url() + else: + flash("File not edited") + M.main_orm_session.flush() + return redirect(url) + + @require_post() + @expose() + def publish_folder(self, folder_id=None, remarks=None): + + '''Controller which publishes the folder. It send update about the publishing of the folder.''' + + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + url = c.app.url + if folder_object: + folder_object.published = True + folder_object.remarks = remarks + mailbox_object = M.Mailbox.query.find({ + 'project_id': c.project._id, 'app_config_id': c.app.config._id}).all() + user_ids = [i.user_id for i in mailbox_object] + admins = [i._id for i in c.project.admins()] + user_ids += admins + user_ids = list(set(user_ids)) + from allura.tasks import mail_tasks + from allura.lib import helpers as h + template_name = '' + try: + for i in user_ids: + user_object = M.User.query.get(_id=i) + template_name = 'forgefiles:/templates/mail.html' + text = g.jinja2_env.get_template(template_name).render(dict( + base_url=config.get('base_url'), user_object=user_object, project=c.project, + remarks=remarks, folder_object=folder_object, project_owner=c.user, + domain=config.get('domain') + )) + email_object = M.EmailAddress.get(claimed_by_user_id=i) + if email_object: + mail_tasks.sendsimplemail.post( + fromaddr=g.noreply, + reply_to=g.noreply, + toaddr=email_object.email, + subject='%s - %s Release Update' % (config.get('site_name'), c.project.name), + message_id=h.gen_message_id(), + text=text) + if folder_object.parent_folder: + url = folder_object.parent_folder.url() + flash('Successfully Published') + except TemplateNotFound: + log.exception('%s Template not found' % (template_name)) + log.info('Folder %s is not published successfully' % (folder_object.folder_name)) + flash('Folder is not published successfully') + return redirect(url) + + @require_post() + @expose() + def disable_folder(self, folder_id=None, status=None): + + '''Controller method to disable the folder.''' + + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + + if status == 'True': + disable_status = True + text = 'disabled' + else: + disable_status = False + text = 'enabled' + if folder_object: + folder_object.disabled = disable_status + '''Disabling Child folders & files of the current folder ''' + + for child_folder_id in folder_object.folder_ids: + child_folder_object = UploadFolder.query.get(_id=ObjectId(child_folder_id)) + if child_folder_object: + child_folder_object.disabled = disable_status + for child_file_id in folder_object.file_ids: + child_file_object = UploadFiles.query.get(_id=ObjectId(child_file_id)) + if child_file_object: + child_file_object.disabled = disable_status + M.main_orm_session.flush() + flash('Folder %s successfully' % (text)) + else: + flash('No folder exists') + + @require_post() + @expose() + def disable_file(self, file_id=None, status=None): + + '''Controller method to disable the file.''' + + file_object = UploadFiles.query.get(_id=ObjectId(file_id)) + if status == 'True': + disable_status = True + text = 'disabled' + else: + disable_status = False + text = 'enabled' + if file_object: + file_object.disabled = disable_status + flash('File %s successfully' % (text)) + else: + flash('No file exists') + + @expose() + def project_file(self): + files_count = UploadFiles.query.find({ + 'project_id': c.project._id, 'linked_to_download': True, 'disabled': False}).count() + return str(files_count) + + @expose('json:') + @require_post() + def subscribe(self, subscribe=None, unsubscribe=None): + + '''Controller method that subscribes an user to the files plugin.''' + + if subscribe: + M.Mailbox.subscribe(type='direct') + elif unsubscribe: + M.Mailbox.unsubscribe() + return { + 'status': 'ok', + 'subscribed': M.Mailbox.subscribed(), + } + + def get_folder_object(self, folder_id=None): + '''Returns the folder object for input folder id''' + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + return folder_object + + @expose('jinja:forgefiles:templates/create_folder.html') + def get_parent_for_create_folder(self, folder_id=None): + '''Returns the parent object of the input folder id''' + folder_object = self.get_folder_object(folder_id) + return dict(folder_object=folder_object) + + @expose('jinja:forgefiles:templates/upload_file.html') + def get_parent_for_upload_file(self, folder_id=None): + '''Returns the parent object of the input folder id''' + folder_object = self.get_folder_object(folder_id) + return dict(folder_object=folder_object) + + def get_folder_file_object(self, object_id=None): + '''Returns corresponding file or folder object for the input id ''' + folder_object = UploadFolder.query.get(_id=ObjectId(object_id)) + file_object = UploadFiles.query.get(_id=ObjectId(object_id)) + return dict(folder_object=folder_object, file_object=file_object) + + @expose('jinja:forgefiles:templates/edit.html') + def get_editable_object(self, object_id=None): + '''Returns object id of the folder or file to be edited''' + object_dict = self.get_folder_file_object(object_id) + return object_dict + + @expose('jinja:forgefiles:templates/delete.html') + def get_deletable_object(self, object_id=None): + '''Returns object id of the folder or file to be deleted''' + object_dict = self.get_folder_file_object(object_id) + return object_dict + + @expose('jinja:forgefiles:templates/publish_folder.html') + def get_publishable_folder(self, folder_id=None): + '''Returns the status and folder object if the folder can be published or not''' + linked_file_object = UploadFiles.query.get(project_id=c.project._id, linked_to_download=True, disabled=False) + parent_folders = get_parent_folders(linked_file_object=linked_file_object) + if folder_id: + folder_object = UploadFolder.query.get(_id=ObjectId(folder_id)) + status = str(folder_object._id) in parent_folders + else: + folder_object = None + status = False + return dict(folder_object=folder_object, status=status) + + @expose() + def _lookup(self, name, *remainder): + ''' Class method which is used to call individual files controller class''' + if not remainder: + argument = name + else: + argument = remainder[-1] + if argument == 'createFolder': + argument = None + return IndividualFilesController(argument), remainder + + +def folder_breadcrumbs(folder_object=None): + ''' Function to create a breadcrumbs for folders ''' + list_object = folder_object.path.split('/') + second_list = [] + length = 0 + urls = {} + for i in list_object: + length += len(i) + folder_object = UploadFolder.query.get(folder_name=i) + urls[str(i)] = str(folder_object.url()) + if length in range(1, (61-len(list_object[-1])+1)): + second_list.append(i) + second_list.append('...') + second_list.append(list_object[-1]) + string = '/'.join(second_list) + if length > 61: + return string, urls + else: + return folder_object.path, urls + + +# handle requests for individual folder,file objects +class IndividualFilesController(BaseController): + """Handle requests for a specific folder/file objects""" + + def __init__(self, arg): + path = request.path.split(c.app.url)[-1].rstrip('/') + if path == arg: + path = arg + path = unquote(path) + arg = unquote(arg) + self.folder_object = UploadFolder.query.find({ + 'project_id': ObjectId(c.project._id), 'folder_name': arg, 'path': path}).first() + self.file_object = UploadFiles.query.find({ + 'project_id': ObjectId(c.project._id), 'filename': arg, 'path': path}).first() + methods = ('create_folder', 'upload_file', 'delete_file', 'delete_folder', 'subscribe') + if (not self.folder_object) and (not self.file_object) and (arg not in methods): + log.exception('No Folder/File object found') + raise exc.HTTPNotFound() + else: + pass + + @expose('jinja:forgefiles:templates/files.html') + @with_trailing_slash + def index(self): + ''' Index method of individual folder/file objects''' + folder_objects = None + file_objects = None + folder_path, urls = '', '' + if self.folder_object: + folder_objects = UploadFolder.query.find({ + 'project_id': c.project._id, 'parent_folder_id': self.folder_object._id}) + folder_objects = folder_objects.sort([('created_date', -1)]).all() + file_objects = UploadFiles.query.find({ + 'project_id': c.project._id, 'parent_folder_id': self.folder_object._id}) + file_objects = file_objects.sort([('created_date', -1)]).all() + folder_path, urls = folder_breadcrumbs(folder_object=self.folder_object) + elif self.file_object: + return FilesController().download_file(filename=self.file_object.filename) + if c.user in c.project.admins(): + M.Mailbox.subscribe(type='direct') + c.subscribe_form = SubscribeForm(thing='files') + tool_subscribed = M.Mailbox.subscribed() + if tool_subscribed: + subscribed = M.Mailbox.subscribed() + else: + subscribed = False + file_object = UploadFiles.query.get(project_id=c.project._id, linked_to_download=True) + parents = get_parent_folders(linked_file_object=file_object) + + return dict(folder_objects=folder_objects, + file_objects=file_objects, folder_object=self.folder_object, file_object=self.file_object, + subscribed=subscribed, parents=parents, folder_path=folder_path, urls=urls) + + @require_post() + @expose() + def create_folder(self, parent_folder_id=None, folder_name=None): + return FilesController().create_folder(parent_folder_id=parent_folder_id, folder_name=folder_name) + + @require_post() + @expose() + def upload_file(self, parent_folder_id=None, filename=None, file_upload=None): + return FilesController().upload_file( + parent_folder_id=parent_folder_id, filename=filename, file_upload=file_upload) + + @require_post() + @expose() + def delete_file(self, file_id=None): + return FilesController().delete_file(file_id=file_id) + + @expose('json:') + @require_post() + def subscribe(self, subscribe=None, unsubscribe=None): + if subscribe: + M.Mailbox.subscribe(type='direct') + elif unsubscribe: + M.Mailbox.unsubscribe() + return { + 'status': 'ok', + 'subscribed': M.Mailbox.subscribed(), + } + + @expose() + def _lookup(self, name, *remainder): + if not remainder: + argument = name + else: + argument = remainder[-1] + return IndividualFilesController(argument), remainder + + +class AttachmentController(att.AttachmentController): + AttachmentClass = UploadFiles + edit_perm = 'update' + + +class AttachmentsController(att.AttachmentsController): + AttachmentControllerClass = AttachmentController diff --git a/ForgeFiles/forgefiles/model/__init__.py b/ForgeFiles/forgefiles/model/__init__.py new file mode 100755 index 0000000..8b738df --- /dev/null +++ b/ForgeFiles/forgefiles/model/__init__.py @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from files import UploadFolder, UploadFiles diff --git a/ForgeFiles/forgefiles/model/files.py b/ForgeFiles/forgefiles/model/files.py new file mode 100755 index 0000000..33062d0 --- /dev/null +++ b/ForgeFiles/forgefiles/model/files.py @@ -0,0 +1,247 @@ +''' This is the Collection module for the Files plugin. Upload, UploadFolder & UploadFile are the collections''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from datetime import datetime + +from urllib import quote +import re + +from ming import schema as S +from ming.orm import Mapper +from ming.orm import FieldProperty, ForeignIdProperty, RelationProperty + +from tg import tmpl_context as c + +from allura.model.artifact import VersionedArtifact +from allura.model.auth import AlluraUserProperty, User +from allura.model.session import project_orm_session, artifact_orm_session +from allura.model.filesystem import File +from allura.model.timeline import ActivityObject +from allura.lib import helpers as h + +README_RE = re.compile(r'^README(\.[^.]*)?$', re.IGNORECASE) + + +class Upload(VersionedArtifact, ActivityObject): + + ''' The Upload collection for files. ''' + + class __mongometa__: + name = 'upload' + session = project_orm_session + type_s = 'Upload' + _id = FieldProperty(S.ObjectId) + filename = FieldProperty(str) + filetype = FieldProperty(str) + project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id) + created_date = FieldProperty(datetime, if_missing=datetime.utcnow) + project = RelationProperty('Project', via='project_id') + file_url = None + + @classmethod + def attachment_class(cls): + return UploadFiles + + @property + def activity_name(self): + return self.filename + + @property + def activity_url(self): + return self.file_url + + +class UploadFolder(VersionedArtifact, ActivityObject): + + ''' The UploadFolder collection is for the Folders. Every folder is an object of this class.''' + + class __mongometa__: + name = 'upload_folder' + session = project_orm_session + + type_s = 'UploadFolder' + _id = FieldProperty(S.ObjectId) + folder_name = FieldProperty(str) + project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id) + parent_folder_id = ForeignIdProperty('UploadFolder') + created_date = FieldProperty(datetime, if_missing=datetime.utcnow) + author = ForeignIdProperty('User', if_missing=lambda: c.user._id) + parent_folder = RelationProperty('UploadFolder', via='parent_folder_id') + project = RelationProperty('Project', via='project_id') + path = FieldProperty(str) + published = FieldProperty(bool, if_missing=False) + remarks = FieldProperty(str) + disabled = FieldProperty(bool, if_missing=False) + folder_ids = FieldProperty([str]) + file_ids = FieldProperty([str]) + + def created_by(self): + + '''Returns the user object of the admin who creaated the folder ''' + + user_obj = User.query.find({'_id': self.author}).first() + return user_obj + + def url(self): + parent_folder = self.parent_folder + if parent_folder: + string = '' + while parent_folder: + string += parent_folder.folder_name + '/' + parent_folder = parent_folder.parent_folder + list_obj = string.rsplit('/')[::-1] + string = '/'.join(list_obj) + string = string.lstrip('/') + url_str = c.app.url + string + '/' + self.folder_name + self.path = string + '/' + self.folder_name + else: + url_str = c.app.url + self.folder_name + self.path = self.folder_name + return quote(url_str) + + def folder_id(self): + return str(self._id) + + @property + def activity_name(self): + return self.folder_name + + @property + def activity_url(self): + parent_folder = self.parent_folder + if parent_folder: + return parent_folder.url() + else: + return c.app.url + + +class UploadFiles(File): + + '''The UploadFiles collection is for the Files. Any file which is uploaded is treated as an Upload object''' + + thumbnail_size = (255, 255) + ArtifactType = Upload + + class __mongometa__: + name = 'upload_files' + session = project_orm_session + indexes = ['artifact_id', 'app_config_id'] + + def before_save(data): + _session = artifact_orm_session._get() + skip_last_updated = getattr(_session, 'skip_last_updated', False) + data['mod_date'] = datetime.utcnow() + if c.project and not skip_last_updated: + c.project.last_updated = datetime.utcnow() + + artifact_id = FieldProperty(S.ObjectId) + app_config_id = FieldProperty(S.ObjectId) + type = FieldProperty(str) + project_id = FieldProperty(S.ObjectId) + parent_folder_id = ForeignIdProperty('UploadFolder') + created_date = FieldProperty(datetime, if_missing=datetime.utcnow) + mod_date = FieldProperty(datetime, if_missing=datetime.utcnow) + author = AlluraUserProperty(if_missing=lambda: c.user._id) + parent_folder = RelationProperty('UploadFolder', via='parent_folder_id') + linked_to_download = FieldProperty(bool, if_missing=False) + path = FieldProperty(str) + disabled = FieldProperty(bool, if_missing=False) + + @property + def file_size(self): + + '''Returns the size of the file''' + + size = self.length + one_gb = 1000000000 + one_mb = 1000000 + one_kb = 1000 + if size > one_gb: + return "{0:.2f} GB".format(float(size)/one_gb) + elif size > one_mb: + return "{0:.2f} MB".format(float(size)/one_mb) + elif size > one_kb: + return "{0:.2f} KB".format(float(size)/one_kb) + else: + return "{} B".format(size) + + @property + def artifact(self): + + '''Returns the Artifact object''' + + return self.ArtifactType.query.get(_id=self.artifact_id) + + def uploaded_by(self): + + '''Returns the user object of the admin who uploads the file''' + + user_obj = User.query.find({'_id': self.author}).first() + return user_obj + + def url(self): + + '''Returns the URL of the uploaded file''' + + parent_folder = self.parent_folder + if parent_folder: + string = '' + while parent_folder: + string += parent_folder.folder_name + '/' + parent_folder = parent_folder.parent_folder + list_obj = string.rsplit('/')[::-1] + string = '/'.join(list_obj) + string = string.lstrip('/') + url_str = c.app.url + string + '/' + self.filename + self.path = string + '/' + self.filename + else: + url_str = c.app.url + self.filename + self.path = self.filename + return quote(url_str) + + def readme(self): + 'returns (filename, unicode text) if a readme file is found' + if README_RE.match(self.filename): + name = self.filename + obj_content = self.rfile().read(self.rfile().length) + return (self.filename, h.really_unicode(obj_content)) + return None, None + + def is_embedded(self): + from tg import request + return self.filename in request.environ.get('allura.macro.att_embedded', []) + + @classmethod + def metadata_for(cls, artifact): + return dict( + artifact_id=artifact._id, + app_config_id=artifact.app_config_id) + + @classmethod + def save_attachment(cls, filename, fp, content_type=None, **kwargs): + filename = h.really_unicode(filename) + original_meta = dict(type="project_file", app_config_id=c.app.config._id, project_id=c.project._id) + original_meta.update(kwargs) + fp.seek(0) + return cls.from_stream( + filename, fp, content_type=content_type, + **original_meta) + + +Mapper.compile_all() diff --git a/ForgeFiles/forgefiles/nf/files/css/files.css b/ForgeFiles/forgefiles/nf/files/css/files.css new file mode 100644 index 0000000..ec32edd --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/css/files.css @@ -0,0 +1,86 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +.upload_buttons{ + float: right; +} +.main_div .btn{ + width: auto; + height: 3em; +} +.main_div table{ + position:relative; + top:20px; + +} +.name_col{ + width:35%; +} +.date_col{ + width:15%; +} + +.size_col{ + width:12%; +} + +.author_col{ + width:25%; +} +.actions_col{ + width:13%; +} +.folder_actions{ + display: flex; + justify-content: space-between; +} +.disable_object, #disable_link{ + color: grey !important; +} +.main_div .fa, .no_objects i{ + font-size: 15px; +} +.file_actions{ + display: flex; + justify-content: space-between; +} +.no_objects{ + position:relative; + top:40px; + text-align: center; +} +.folder_dis .icon { + color: grey !important; +} +.file_dis .icon { + color: grey !important; +} +.folder_name_label{ + font-size: 15px !important; +} + + +/* CSS For Popups */ +.modal-header .close{ + position: absolute;top: 10px;right: 10px;cursor: pointer; +} +.modal-form-error{ + margin-left: unset !important; +} + diff --git a/ForgeFiles/forgefiles/nf/files/js/create_folder.js b/ForgeFiles/forgefiles/nf/files/js/create_folder.js new file mode 100644 index 0000000..f293612 --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/create_folder.js @@ -0,0 +1,77 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$('#admin_modal_title').hide(); +$('#folder_id').focus(); + +function validateFolderForm(){ + var folder_id = document.getElementById('folder_id'); + var folder_name = $(folder_id).val().trim(); + var error = $('#error_message'); + + var flag; + flag = validateName(folder_name); + + if ( folder_name.length === 0){ + $(error).text('Please enter folder name.'); + return false; + } + else{ + return true; + } + + } + +function validateName(folder_name){ + + var error = $('#error_message'); + var regular_exp = new RegExp("^[a-zA-Z0-9_ +.,=#~@!()\\[\\]-]+$"); + var validation_msg; + if(folder_name.length===0){validation_msg="Please enter folder name."} + else{ + if(folder_name.slice(0,1)==="."){ + validation_msg='Folder name cannot start with ".".'} + else{ + if(folder_name.slice(0,1)===" "){ + validation_msg="Folder name cannot start with a space."} + else{ + if(folder_name.slice(-1)===" "){ + validation_msg="Folder name cannot end with a space."} + else{if(!regular_exp.test(folder_name)){validation_msg='Folder name cannot contain characters like ($/\"%^&*`|?<>:;).'} + else{validation_msg=true}}}}} + + return validation_msg; + } + +$('#folder_id').keyup(function(){ + var folder_name = $('#folder_id').val(); + var error = $('#error_message'); + var flag; + flag = validateName(folder_name); + if (flag != true){ + $(error).text(flag); + $('#submit_btn').attr('disabled',true); + } + else{ + $(error).text(''); + $('#submit_btn').attr('disabled',false); + } +}); + + diff --git a/ForgeFiles/forgefiles/nf/files/js/delete.js b/ForgeFiles/forgefiles/nf/files/js/delete.js new file mode 100644 index 0000000..f53b679 --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/delete.js @@ -0,0 +1,33 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$('#admin_modal_title').hide(); + + function ConfirmDeleteFolder(){ + var obj_id = document.getElementById('delete_id'); + var confirm_delete = $(obj_id).val(); + var error = $('#error_message'); + if((confirm_delete === "DELETE")){ + return true; + } + else{ + $(error).text('You must confirm with the word DELETE'); + return false; + } + } diff --git a/ForgeFiles/forgefiles/nf/files/js/edit.js b/ForgeFiles/forgefiles/nf/files/js/edit.js new file mode 100644 index 0000000..52d64cd --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/edit.js @@ -0,0 +1,82 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$('#admin_modal_title').hide(); +$('#obj_id').select(); +var obj_type = $('#obj_type').val(); + +function validateEditFolderForm(){ + var folder_obj = document.getElementById('obj_id'); + var folder_name = $(folder_obj).val().trim(); + var error = $('#error_message'); + var flag; + flag = validateName(folder_name); + if (folder_name.length === 0){ + if(obj_type == 'folder') + $(error).text('Please enter folder name'); + else if(obj_type == 'file') + $(error).text('Please enter file name'); + return false; + } + else{ + return true; + } + + } + +//#################// + +function validateName(object_name){ + + var error = $('#error_message'); + var regular_exp=new RegExp("^[a-zA-Z0-9_ +.,=#~@!()\\[\\]-]+$"); + var validation_msg; + if(object_name.length===0){ if(obj_type == 'folder') {validation_msg="Please enter folder name."} else {validation_msg="Please enter file name."} } + else{ + if(object_name.slice(0,1)==="."){ + if(obj_type == 'folder') {validation_msg='Folder name cannot start with ".".'} else {validation_msg='File name cannot start with ".".'} } + else{ + if(object_name.slice(0,1)===" "){ + if(obj_type == 'folder') {validation_msg="Folder name cannot start with a space."} else {validation_msg="File name cannot start with a space."} } + else{ + if(object_name.slice(-1)===" "){ + if(obj_type == 'folder') {validation_msg="Folder name cannot end with a space."} else {validation_msg="File name cannot end with a space."} } + else{if(!regular_exp.test(object_name)){ + if(obj_type == 'folder') {validation_msg='Folder name cannot contain characters like ($/\"%^&*`|?<>:;).'} + else {validation_msg='File name cannot contain characters like ($/\"%^&*`|?<>:;).'} } + else{validation_msg=true}}}}} + + return validation_msg; + } + +$('#obj_id').keyup(function(){ + var obj_name = $('#obj_id').val(); + var error = $('#error_message'); + var flag; + flag = validateName(obj_name); + if (flag != true){ + $(error).text(flag); + $('#submit_btn').attr('disabled',true); + } + else{ + $(error).text(''); + $('#submit_btn').attr('disabled',false); + } +}); + diff --git a/ForgeFiles/forgefiles/nf/files/js/files.js b/ForgeFiles/forgefiles/nf/files/js/files.js new file mode 100644 index 0000000..53a1684 --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/files.js @@ -0,0 +1,94 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$.ajaxPrefilter(function( options, originalOptions, jqXHR ) { options.async = true; }); + +function removeModalContent(){ + $('#delete_folder').val(' '); + $('#error_message_delete_folder').remove(); +} + + + function ConfirmDisableFolder(folderID,status,parent_status,url) + { + if (status == 'True' && parent_status == 'True'){ + alert('Please enable parent folder of this folder'); + return false; + } + else{ + if (status == 'False'){ + var confirm_resp = confirm("Are you sure you want to disable?"); + var disable_status = 'True'; + } + else if(status == 'True'){ + var confirm_resp = confirm("Are you sure you want to enable?"); + var disable_status = 'False'; + } + if (confirm_resp){ + $.post(url, {'folder_id':folderID, 'status':disable_status}, function() { + location.reload(); + }); + } + else + return false; + true + } + } + function ConfirmDisableFile(fileID,status,parent_status,url) + { + if (status == 'True' && parent_status == 'True'){ + alert('Please enable parent folder of this file'); + return false; + } + else{ + if (status == 'False'){ + var confirm_resp = confirm("Are you sure you want to disable?"); + var disable_status = 'True'; + } + else if(status == 'True'){ + var confirm_resp = confirm("Are you sure you want to enable?"); + var disable_status = 'False'; + } + if (confirm_resp){ + $.post(url, {'file_id':fileID, 'status':disable_status}, function() { + location.reload(); + }); + } + else + return false; + } + } + + function ConfirmLinkFile(fileID,linked_to_download,url) + { + + if(linked_to_download === 'True'){ + var confirm_resp = confirm("This file is already linked to Downloads. Do you want to unlink it?"); + var link_status = 'False'; + } + else{ + var confirm_resp = confirm("Are you sure you want to link to the Downloads?"); + var link_status = 'True'; + } + $.post(url, {'file_id':fileID, 'status':link_status}, function() { + location.reload(); + }) + } + + diff --git a/ForgeFiles/forgefiles/nf/files/js/publish_folder.js b/ForgeFiles/forgefiles/nf/files/js/publish_folder.js new file mode 100644 index 0000000..a69ed78 --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/publish_folder.js @@ -0,0 +1,46 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$('#admin_modal_title').hide(); +$('#remarks_id').focus(); + + + function ConfirmPublishFolder(){ + var remarks = document.getElementById('remarks_id'); + var release_notes = $(remarks).val().trim(); + var error = $('#error_message'); + var parent = document.getElementById('parent_publish_status'); + var parent_publish_status = $(parent).val(); + var current_folder = document.getElementById('publish_status'); + var publish_status = $(current_folder).val(); + var submit_btn = $('#submit_btn'); + if(release_notes.length === 0){ + $(error).text('Please enter release notes'); + return false; + } + else if(parent_publish_status === 'False'){ + $(submit_btn).attr('disabled', true); + $(error).text('To publish this folder, please link a file under it to the Download button'); + return false; + } + else{ + return true; + } + + } diff --git a/ForgeFiles/forgefiles/nf/files/js/upload_files.js b/ForgeFiles/forgefiles/nf/files/js/upload_files.js new file mode 100644 index 0000000..c00c582 --- /dev/null +++ b/ForgeFiles/forgefiles/nf/files/js/upload_files.js @@ -0,0 +1,36 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +$('#admin_modal_title').hide(); + + function validateFileForm(){ + var file_input = document.getElementById('file_input'); + var file_path = file_input.value.split('\\').pop(); + var filename = $('#filename'); + var file_val = $(file_input).val(); + var error = $('#error_message'); + if (file_val.length === 0){ + $(error).text('Please upload a file'); + return false; + } + else{ + $(filename).val(file_path); + return true; + } + } diff --git a/ForgeFiles/forgefiles/templates/create_folder.html b/ForgeFiles/forgefiles/templates/create_folder.html new file mode 100644 index 0000000..fef917e --- /dev/null +++ b/ForgeFiles/forgefiles/templates/create_folder.html @@ -0,0 +1,58 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} + +<script type="text/javascript" src="{{g.app_static('js/create_folder.js')}}" async="async"></script> + + <h1 class="title" > + <span> Create Folder </span> + </h1> + + <div id="folder_popup" class="" role="dialog" > + <form {% if folder_object %} action="{{folder_object.url()}}/create_folder" {% else %} action="{{c.app.url}}create_folder" {% endif %} method="post" enctype="multipart/form-data" onsubmit="return validateFolderForm();"> + <div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <a class="icon close" href="#" title="Close" data-dismiss="modal"><i class="fa fa-close"></i></a> + </div> + <div class="modal-body grid-13"> + <p> + + <label for="folder_name">Folder Name:</label> + <input type="text" id="folder_id" name="folder_name" maxlength="260" value="" > + + <span id="error_message" class="modal-form-error"></span> + </p> + </div> + <input type="hidden" name="parent_folder_id" {% if folder_object %} value="{{folder_object._id}}" {% else %} value=None {% endif %} /> + <div class="modal-footer grid-13"> + <p> + <input type="submit" id="submit_btn" value="Save"/> + <a href="#" class="close">Cancel</a> + </p> + </div> + </div> + </div> + {{lib.csrf_token()}} + </form> + </div> + + diff --git a/ForgeFiles/forgefiles/templates/delete.html b/ForgeFiles/forgefiles/templates/delete.html new file mode 100644 index 0000000..229450d --- /dev/null +++ b/ForgeFiles/forgefiles/templates/delete.html @@ -0,0 +1,62 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} + +<script type="text/javascript" src="{{g.app_static('js/delete.js')}}"></script> + + <h1 class="title" > + <span> Are you sure you want to delete this {% if folder_object %} folder? {% elif file_object %} file? {% endif %} </span> + </h1> + +<div id="delete_folder_popup" > +<form id ='delete_folder_form' method="post" {% if folder_object %} action="{{c.app.url}}delete_folder" {% elif file_object %} action="{{c.app.url}}delete_file" {% endif %} onsubmit="return ConfirmDeleteFolder();" > +<div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <a class="icon close" href="#" title="Close" data-dismiss="modal"><i class="fa fa-close"></i></a> + </div> + <div class="modal-body grid-13"> + <p> + + <label for="delete">This is an irreversible action. If you are sure then type 'DELETE'.</label> + <input type="text" id="delete_id" autofocus /> + + <span id="error_message" class="modal-form-error"></span> + </p> + </div> + {% if folder_object %} + <input type="hidden" name="folder_id" id='delete_folder_id' value="{{folder_object._id}}" /> + {% elif file_object %} + <input type="hidden" name="file_id" id='delete_file_id' value="{{file_object._id}}" /> + {% endif %} + <div class="modal-footer grid-13"> + <p> + <input type="submit" id="submit_btn" value="Ok"/> + <a href="#" class="close">Cancel</a> + </p> + </div> + </div> +</div> +{{lib.csrf_token()}} +</form> +</div> + + diff --git a/ForgeFiles/forgefiles/templates/edit.html b/ForgeFiles/forgefiles/templates/edit.html new file mode 100644 index 0000000..7379872 --- /dev/null +++ b/ForgeFiles/forgefiles/templates/edit.html @@ -0,0 +1,78 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} + +<script type="text/javascript" src="{{g.app_static('js/edit.js')}}"></script> + + <h1 class="title" > + <span> Edit {% if folder_object %} Folder {% elif file_object %} File {% endif %} Name </span> + </h1> + +<!-- popup for edit folder--> +<div id="edit_folder_popup" > + <form id ='edit_folder_form' method="post" + {% if folder_object %} + action="{{c.app.url}}edit_folder" + {% elif file_object %} + action="{{c.app.url}}edit_file" + {% endif %} + onsubmit="return validateEditFolderForm();" > + <div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <a class="icon close" href="#" title="Close" data-dismiss="modal"><i class="fa fa-close"></i></a> + </div> + + {% if folder_object %} + <div class="modal-body grid-13"> + <p> + <label for="folder_name">Folder Name:</label> + <input type="text" id="obj_id" name="folder_name" maxlength="260" value="{{folder_object.folder_name}}" /> + <input type="hidden" id="obj_type" value="folder" /> + <span id="error_message" class="modal-form-error"></span> + </p> + </div> + <input type="hidden" name="folder_id" id='edit_folder_id' value="{{folder_object._id}}" /> + {% elif file_object %} + <div class="modal-body grid-13"> + <p> + <label for="file_name">File Name:</label> + <input type="text" id="obj_id" name="file_name" maxlength="260" value="{{file_object.filename}}" /> + <input type="hidden" id="obj_type" value="file" /> + <span id="error_message" class="modal-form-error"></span> + </p> + </div> + <input type="hidden" name="file_id" id='edit_file_id' value="{{file_object._id}}" /> + {% endif %} + + <div class="modal-footer grid-13"> + <p> + <input type="submit" id="submit_btn" value="Save"/> + <a href="#" class="close">Cancel</a> + </p> + </div> + </div> + </div> + {{lib.csrf_token()}} + </form> +</div> + + diff --git a/ForgeFiles/forgefiles/templates/files.html b/ForgeFiles/forgefiles/templates/files.html new file mode 100755 index 0000000..91a0d41 --- /dev/null +++ b/ForgeFiles/forgefiles/templates/files.html @@ -0,0 +1,193 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% extends g.theme.master %} +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} +{% import 'forgeactivity:templates/macros.html' as am with context %} + +{% do g.register_app_css('css/files.css', compress=False) %} + +{% block title %} +{{c.project.name}} / {{c.app.config.options.mount_label}} / File Upload +{% endblock %} + +{% macro path_links(parts, urls) %} + {% if parts != [''] %} + <a href="{{c.app.url}}"><b class="fa fa-folder-open-o" title="Root directory"></b></a> / + {% endif %} + {% for part in parts %} + {% if part != '...' and not loop.last %} + <a href="{{ urls[part] }}"> + {{part}} + </a> / + {% else %} + {{ part }} {% if not loop.last %} / {% endif %} + {% endif %} + {% endfor %} +{% endmacro %} + +{% block header %} +Files {% if folder_object %} - {{path_links(folder_path.split('/'), urls)}} {% endif %} +{% endblock %} + +{% block actions %} +{% if c.user and c.user != c.user.anonymous() %} +{{c.subscribe_form.display(value=subscribed, action='subscribe', style='icon')}} +{% endif %} +{% endblock %} + +{% block content %} + + +{% if not c.user.is_anonymous() %} + +<div class="main_div"> + + {% if h.has_access(c.project, 'admin')() %} + <div class="upload_buttons" > + <a {% if folder_object %} href="{{c.app.url}}get_parent_for_create_folder?folder_id={{folder_object._id}}" {% else %} href="{{c.app.url}}get_parent_for_create_folder" {% endif %} class="admin_modal"> + <button id="create_folder" type= "button" class="btn btn-info btn-lg" {% if folder_object.disabled %} disabled {% endif %} >Create Folder</button> + </a> + <a {% if folder_object %} href="{{c.app.url}}get_parent_for_upload_file?folder_id={{folder_object._id}}" {% else %} href="{{c.app.url}}get_parent_for_upload_file" {% endif %} class="admin_modal"> + <button id="add_files" type= "button" class="btn btn-info btn-lg" {% if folder_object.disabled %} disabled {% endif %} >Add File</button> + </a> + </div> + {% endif %} + + + {% if file_objects or folder_objects %} + <table> + <colgroup> + <col class="name_col"> + <col class="date_col"> + <col class="size_col"> + <col class="author_col"> + {% if h.has_access(c.project, 'admin')() %} + <col class="actions_col"> + {% endif %} + </colgroup> + <thead> + <tr> + <th>File</th> + <th>Date</th> + <th>Size</th> + <th>Author</th> + {% if h.has_access(c.project, 'admin')() %} + <th>Actions</th> + {% endif %} + + </tr> + </thead> + <tbody> + {% for folder in folder_objects %} + <tr {% if not folder.disabled %} title="{{folder.folder_name}}" {% else %} class="{{folder.folder_name}}" + title="{{folder.folder_name}} is disabled" {% endif %} > + <td {% if folder.disabled %} class="folder_dis" {% endif %} title="Click to enter {{folder.folder_name}}"> + {% set icon_name = 'folder' %} + + <a class="icon" href="{{folder.url()}}" title="Click to enter {{folder.folder_name}}"> + <i class="fa fa-folder"></i> {{folder.folder_name|truncate(30)}} + </a> + + </td> + <td>{{lib.abbr_date(folder.created_date)}}</td> + <td></td> + <td title="{{folder.created_by().display_name}}">{{folder.created_by().display_name|truncate(30)}}</td> + {% if h.has_access(c.project, 'admin')() %} + <td> + <div class="folder_actions"> + <a data-toggle="tooltip" {% if not folder.disabled %} title="Publish" class="admin_modal" href= "{{c.app.url}}get_publishable_folder?folder_id={{folder._id}}" {% else %} class="disable_object" {% endif %} ><i class= "fa fa-share" {% if folder.disabled %} onClick="return false"{% endif %} ></i></a> + + <a data-toggle="tooltip " {% if not folder.disabled %} class="edit_icon admin_modal" href="{{c.app.url}}get_editable_object?object_id={{folder._id}}" title="Edit" {% else %} class="disable_object" {% endif %}><i class="fa fa-edit" data-toggle='modal'></i></a> + <a data-toggle="tooltip " {% if not folder.disabled %} class="delete_icon admin_modal" href="{{c.app.url}}get_deletable_object?object_id={{folder._id}}" title="Delete" {% else %} class="disable_object" {% endif %}><i class="fa fa-trash-o" data-toggle='modal'></i></a> + + + <a href="#" data-toggle="tooltip" {% if folder.disabled %} title="Enable" {% else %} title="Disable" {% endif %} onclick="ConfirmDisableFolder('{{ folder._id }}', '{{folder.disabled}}', '{{folder.parent_folder.disabled}}','{{c.app.url}}disable_folder')" ><i {% if folder.disabled %} class="fa fa-undo" {% else %} class="fa fa-ban" {% endif %}></i></a> + + + </div> + </td> + {% endif%} + + </tr> + {% endfor %} + + {% for file in file_objects %} + <tr {% if not file.disabled %} title="{{file.filename}}" {% set url = file.url() %} {% else %} + title="{{file.filename}} is disabled" {% set url = '#' %} {% endif %} > + <td {% if file.disabled %} class="file_dis" {% endif %} > + {% set icon_name = 'file' %} + <a class="icon" {% if not file.disabled %} href="{{file.url()}}" title="Click to download {{file.filename}}" {% else %} href="#" {% endif %} > + <i class="fa fa-file-o"></i> {{file.filename|truncate(30)}} + </a> + + + </td> + <td>{{lib.abbr_date(file.created_date)}}</td> + <td>{{file.file_size}}</td> + <td title="{{file.uploaded_by().display_name}}">{{file.uploaded_by().display_name|truncate(30)}}</td> + {% if h.has_access(c.project, 'admin')() %} + <td> + <div class="file_actions"> + <a data-toggle="tooltip" {% if not file.disabled %} href= "#" {% if not file.linked_to_download%} title="Link" {% else %} title="Unlink" {% endif %} class="link_icon" onclick="ConfirmLinkFile('{{ file._id }}', '{{ file.linked_to_download }}', '{{c.app.url}}link_file')" {% else %} class="disable_object" {% endif %} ><i class= "fa fa-link" {% if file.linked_to_download %} id="disable_link" {% endif %}></i></a> + <a data-toggle="tooltip " {% if not file.disabled %} class="edit_icon admin_modal" href="{{c.app.url}}get_editable_object?object_id={{file._id}}" title="Edit" {% else %} class="disable_object" {% endif %}><i class="fa fa-edit" data-toggle='modal' ></i></a> + + <a data-toggle="tooltip " {% if not file.disabled %} class="delete_icon admin_modal" href="{{c.app.url}}get_deletable_object?object_id={{file._id}}" title="Delete" {% else %} class="disable_object" {% endif %}><i class="fa fa-trash-o" data-toggle='modal' {% if not file.disabled %} {% endif %} ></i></a> + + <a href="#" data-toggle="tooltip" {% if file.disabled %} title="Enable" {% else %} title="Disable" {% endif %} onclick="ConfirmDisableFile('{{ file._id }}', '{{file.disabled}}', '{{file.parent_folder.disabled}}','{{c.app.url}}disable_file')" ><i {% if file.disabled %} class="fa fa-undo" {% else %} class="fa fa-ban" {% endif %}></i></a> + + </div> + </td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> + {% else %} + <div class="no_objects" > + <i>No file or folder has been uploaded yet.</i> + </div> + {% endif %} + + {% for file in file_objects %} + {% if not file.disabled %} + {% set name, text = file.readme() %} + {% if name %} + <h1 id="readme"> Read Me </h1> + {{h.render_any_markup(name, text)}} + {% endif %} + {% endif %} + {% endfor %} + + +</div> + +{% else %} + <p> Please login to upload a project file </p> +{% endif %} + +{% endblock %} + + +{% block extra_js %} + +<script type="text/javascript" src="{{g.app_static('js/files.js')}}"></script> + +{% endblock %} + diff --git a/ForgeFiles/forgefiles/templates/mail.html b/ForgeFiles/forgefiles/templates/mail.html new file mode 100644 index 0000000..9466b5a --- /dev/null +++ b/ForgeFiles/forgefiles/templates/mail.html @@ -0,0 +1,34 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +#} + +Dear {{user_object.display_name}}, + + +A new release of {{project.name}} is available now! + +Release Notes from project owner {{project_owner.display_name}} : {{remarks}} + +Please check out the release @ <{{base_url}}{{folder_object.url()}}> + + +--- + +Sent from {{domain}} because you indicated interest in <{{ base_url }} > + +To unsubscribe from further messages, please visit <{{ base_url }}/auth/subscriptions/> diff --git a/ForgeFiles/forgefiles/templates/publish_folder.html b/ForgeFiles/forgefiles/templates/publish_folder.html new file mode 100644 index 0000000..c4bd240 --- /dev/null +++ b/ForgeFiles/forgefiles/templates/publish_folder.html @@ -0,0 +1,69 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% do g.register_css('css/files.css', compress=False) %} + +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} + + <h1 class="title" > + <span> Publish Folder </span> + </h1> + + <!-- popup for publish folder--> +<div id="publish_folder_popup" > +<form id ='publish_folder_form' method="post" action="{{c.app.url}}publish_folder" onsubmit="return ConfirmPublishFolder();" > +<div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <a class="icon close" href="#" title="Close" data-dismiss="modal"><i class="fa fa-close"></i></a> + </div> + <div class="modal-body grid-13"> + <p> + + <label for="folder_name" class="folder_name_label" >Release Notes:</label> + <textarea type="text-area" id="remarks_id" name="remarks" rows="4" cols="50" maxlength="100" {% if not status %} disabled {% endif %} ></textarea><br /> + + <span id="error_message" class="modal-form-error"> + {% if not status %} To publish this folder, please link a file under it to the Download button {% endif %} + {% if status and folder_object.published %} This folder has been already published. Please publish again only if there are any significant changes. {% endif %} + </span> + </p> + </div> + <input type="hidden" name="folder_id" id='publish_folder_id' value="{{folder_object._id}}" /> + <input type="hidden" id='parent_publish_status' value="{{status}}" /> + <input type="hidden" id='publish_status' value="{{folder_object.published}}" /> + + <div class="modal-footer grid-13"> + <p> + <input type="submit" id="submit_btn" value="Ok" {% if not status %} disabled {% endif %} /> + <a href="#" class="close">Cancel</a> + </p> + </div> + </div> +</div> +{{lib.csrf_token()}} +</form> +</div> + + +<script type="text/javascript" src="{{g.app_static('js/publish_folder.js')}}"></script> + + + diff --git a/ForgeFiles/forgefiles/templates/upload_file.html b/ForgeFiles/forgefiles/templates/upload_file.html new file mode 100644 index 0000000..1dd109d --- /dev/null +++ b/ForgeFiles/forgefiles/templates/upload_file.html @@ -0,0 +1,55 @@ +{#- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +-#} + +{% import 'allura:templates/jinja_master/lib.html' as lib with context %} +<script type="text/javascript" src="{{g.app_static('js/upload_files.js')}}"></script> + + + <h1 class="title" > + <span> Add File </span> + </h1> + + <!-- popup for add files--> + <div id="file_popup" > + <form {% if folder_object %} action="{{folder_object.url()}}/upload_file" {% else %} action="{{c.app.url}}upload_file" {% endif %} method="post" enctype="multipart/form-data" onsubmit="return validateFileForm()" > + <div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <a class="icon close" href="#" title="Close" data-dismiss="modal"><i class="fa fa-close"></i></a> + </div> + <div class="modal-body grid-13"> + <p> + <input type="file" id="file_input" name="file_upload" /> + <span id="error_message" class="modal-form-error"></span> + </p> + </div> + <input type="hidden" name="parent_folder_id" {% if folder_object %} value="{{folder_object._id}}" {% else %} value="None" {% endif %} /> + <input type="hidden" name="filename" id="filename" value="" /> + <div class="modal-footer grid-13"> + <p> + <input type="submit" id="submit_btn" value="Save"/> + <a href="#" class="close">Cancel</a> + </p> + </div> + </div> + </div> + {{lib.csrf_token()}} + </form> + </div> diff --git a/ForgeFiles/forgefiles/tests/__init__.py b/ForgeFiles/forgefiles/tests/__init__.py new file mode 100644 index 0000000..77505f1 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/__init__.py @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + diff --git a/ForgeFiles/forgefiles/tests/functional/__init__.py b/ForgeFiles/forgefiles/tests/functional/__init__.py new file mode 100644 index 0000000..77505f1 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/functional/__init__.py @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + diff --git a/ForgeFiles/forgefiles/tests/functional/test_root.py b/ForgeFiles/forgefiles/tests/functional/test_root.py new file mode 100644 index 0000000..569d8a7 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/functional/test_root.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from tg import tmpl_context as c +from nose.tools import assert_true,assert_not_equal,assert_equals + +from allura import model as M +from alluratest.controller import TestController + + +from forgefiles.model.files import UploadFolder +from forgefiles.model.files import UploadFiles + +from testfixtures import TempDirectory + + +class TestFiles(TestController): + def setUp(self): + TestController.setUp(self) + + def test_files(self): + c.user = M.User.by_username('test-admin') + r = self.app.get('/files/') + assert 'p/test' in r + + def test_create_folder(self): + c.user = M.User.by_username('test-admin') + self.app.get('/files/') + data1 = {'folder_name': 'NewTestFolder'} + folder_object = self.app.post('/p/test/files/create_folder/', data1) + assert folder_object is not None + + def test_upload_file(self): + c.user = M.User.by_username('test-admin') + self.app.get('/files/') + dir = TempDirectory() + path = dir.write('myfile.txt', b'Testing Upload') + with open(path, 'rb') as f: + file_upload = [('file_upload', 'myfile.txt', f.read())] + filename_dict = {'filename':'myfile.txt'} + file_object = self.app.post('/p/test/files/upload_file', filename_dict, upload_files=file_upload) + dir.cleanup() + assert file_object is not None + + def test_edit_folder(self): + folder = create_folder(self) + folder_object = UploadFolder.query.get(folder_name=folder.folder_name) + data1 = {'folder_id': str(folder_object._id), 'folder_name': 'NewFolderName'} + self.app.post('/p/test/files/edit_folder', data1) + resp = self.app.get('/files/') + assert 'NewFolderName' in resp + + def test_edit_file(self): + file_object = upload_file(self) + db_file_object = UploadFiles.query.get(filename=file_object.filename) + data1 = {'file_id': str(db_file_object._id), 'file_name': 'NewFileName'} + self.app.post('/p/test/files/edit_file', data1) + resp = self.app.get('/files/') + assert 'NewFileName' in resp + + def test_publish_folder(self): + create_folder(self) + folder_object = UploadFolder.query.get(folder_name='TestFolder') + data1 = {'folder_id': str(folder_object._id), 'remarks': 'Publishing new Version'} + self.app.post('/p/test/files/publish_folder', data1) + resp = self.app.get('/files/') + print(folder_object.published) + assert_equals(folder_object.published, True) + + def test_link_file(self): + file_object = upload_file(self) + db_file_object = UploadFiles.query.get(filename=file_object.filename) + data1 = {'file_id': str(db_file_object._id)} + self.app.post('/p/test/files/link_file', data1) + resp = self.app.get('/files/') + assert_true(str(db_file_object.linked_to_download) in resp) + + def test_disable_folder(self): + create_folder(self) + folder_object = UploadFolder.query.get(folder_name='TestFolder') + data1 = {'folder_id': str(folder_object._id), 'status': 'True'} + self.app.post('/p/test/files/disable_folder', data1) + resp = self.app.get('/files/') + assert_true(str(folder_object.disabled) in resp) + + def test_disable_file(self): + file_object = upload_file(self) + db_file_object = UploadFiles.query.get(filename=file_object.filename) + data1 = {'file_id': str(db_file_object._id), 'status': 'True'} + self.app.post('/p/test/files/disable_file', data1) + resp = self.app.get('/files/') + assert_true(str(db_file_object.disabled) in resp) + + def test_delete_folder(self): + create_folder(self) + folder_object = UploadFolder.query.get(folder_name='TestFolder') + data1 = {'folder_id': str(folder_object._id)} + self.app.post('/p/test/files/delete_folder', data1) + new_folder_object = UploadFolder.query.get(_id=folder_object._id) + assert new_folder_object is None + + def test_delete_file(self): + file_object = upload_file(self) + db_file_object = UploadFiles.query.get(filename=file_object.filename) + data1 = {'file_id': str(db_file_object._id)} + self.app.post('/p/test/files/delete_file', data1) + new_file_object = UploadFiles.query.get(_id=db_file_object._id) + assert new_file_object is None + + +def create_folder(self): + c.user = M.User.by_username('test-admin') + self.app.get('/files/') + data = {'folder_name': 'TestFolder'} + self.app.post('/p/test/files/create_folder/', data) + folder_object = UploadFolder.query.get(folder_name='TestFolder') + return folder_object + + +def upload_file(self): + c.user = M.User.by_username('test-admin') + self.app.get('/files/') + dir = TempDirectory() + path = dir.write('myfile.txt', b'Testing Upload') + with open(path, 'rb') as f: + file_upload = [('file_upload', 'myfile.txt', f.read())] + filename_dict = {'filename':'myfile.txt'} + self.app.post('/p/test/files/upload_file', filename_dict, upload_files=file_upload) + file_object = UploadFiles.query.get(filename='myfile.txt') + dir.cleanup() + return file_object diff --git a/ForgeFiles/forgefiles/tests/model/__init__.py b/ForgeFiles/forgefiles/tests/model/__init__.py new file mode 100644 index 0000000..b91b746 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/model/__init__.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from tg import tmpl_context as c +from ming.orm.ormsession import ThreadLocalORMSession + +from allura.websetup import bootstrap +from allura.lib import helpers as h +from allura.lib import plugin +from allura import model as M +from alluratest.controller import setup_basic_test + + +def setUp(): + setup_basic_test() + + +class FilesTestWithModel(object): + + def setUp(self): + bootstrap.wipe_database() + project_reg = plugin.ProjectRegistrationProvider.get() + c.user = bootstrap.create_user('Test User') + neighborhood = M.Neighborhood(name='Projects', url_prefix='/p/', + features=dict(private_projects=False, + max_projects=None, + css='none', + google_analytics=False)) + project_reg.register_neighborhood_project(neighborhood, [c.user]) + c.project = neighborhood.register_project('test', c.user) + c.project.install_app('Files', 'files') + ThreadLocalORMSession.flush_all() + h.set_context('test', 'files', neighborhood='Projects') + + def tearDown(self): + ThreadLocalORMSession.close_all() + diff --git a/ForgeFiles/forgefiles/tests/model/test_files.py b/ForgeFiles/forgefiles/tests/model/test_files.py new file mode 100644 index 0000000..6af03e2 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/model/test_files.py @@ -0,0 +1,54 @@ +'''This module is added for testing the files model ''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from nose.tools import assert_equal, assert_true, assert_false + +from forgefiles.tests.model import FilesTestWithModel +from forgefiles.model.files import UploadFolder +from forgefiles.model.files import UploadFiles + + + +class TestUpload(FilesTestWithModel): + ''' Test class for UploadFolder & uploadFiles model''' + + def test_upload_folder(self): + '''Creates an object of the UploadFolder Collction and tests its fields''' + upload_folder = UploadFolder + upload_folder.folder_name = 'testFolder' + upload_folder.published = True + upload_folder.remark = 'Publishing new Version' + upload_folder.disabled = False + assert_equal(upload_folder.folder_name, 'testFolder') + assert_true(upload_folder.published) + assert_equal(upload_folder.remark, 'Publishing new Version') + assert_false(upload_folder.disabled) + + def test_upload_file(self): + '''Creates an object of the UploadFiles Collction and tests its fields''' + + upload_file = UploadFiles + upload_file.filename = 'testFile' + upload_file.filetype = 'project_file' + upload_file.file_url = 'TestFolder/testFile' + upload_file.linked_to_download = True + upload_file.published = False + assert_equal(upload_file.filename, 'testFile') + assert_equal(upload_file.filetype, 'project_file') + assert_true(upload_file.linked_to_download) + assert_false(upload_file.published) diff --git a/ForgeFiles/forgefiles/tests/test_files_roles.py b/ForgeFiles/forgefiles/tests/test_files_roles.py new file mode 100644 index 0000000..203f331 --- /dev/null +++ b/ForgeFiles/forgefiles/tests/test_files_roles.py @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from nose.tools import assert_equal +from tg import tmpl_context as c, app_globals as g + +from alluratest.controller import setup_basic_test, setup_global_objects +from allura import model as M +from allura.lib import security +from allura.tests import decorators as td +from allura.lib import helpers as h + + +def setUp(): + setup_basic_test() + setup_with_tools() + + [email protected]_tool('u/test-user-1', 'files') [email protected]_user_project('test-user-1') +def setup_with_tools(): + setup_global_objects() + h.set_context('test', neighborhood='Projects') + c.project.install_app('files', 'files') + g.set_app('files') + + +def test_role_assignments(): + admin = M.User.by_username('test-admin') + user = M.User.by_username('test-user') + anon = M.User.anonymous() + + def check_access(perm): + pred = security.has_access(c.app, perm) + return pred(user=admin), pred(user=user), pred(user=anon) + assert_equal(check_access('read'), (True, True, True)) + assert_equal(check_access('create'), (True, False, False)) + assert_equal(check_access('update'), (True, False, False)) + assert_equal(check_access('delete'), (True, False, False)) diff --git a/ForgeFiles/setup.py b/ForgeFiles/setup.py new file mode 100755 index 0000000..a3dbaa9 --- /dev/null +++ b/ForgeFiles/setup.py @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# !/bin/python +from setuptools import setup + +setup(name='ForgeFiles', + version='0.1.0', + description="", + packages=['forgefiles'], + include_package_data=True, + zip_safe=True, + entry_points=""" + # -*- Entry points: -*- + [allura] + Files=forgefiles.files_main:FilesApp + """, + )
