You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2021/07/01 22:05:47 UTC
[allura] 01/07: [#8368] Snippets for the new Files App contribution
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 <Sa...@in.bosch.com>
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()
+
+
+@td.with_tool('u/test-user-1', 'files')
+@td.with_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
+ """,
+ )