[ckan-changes] commit/ckanextiati: 5 new changesets

Bitbucket commits-noreply at bitbucket.org
Mon Nov 7 18:56:34 UTC 2011


5 new commits in ckanextiati:


https://bitbucket.org/okfn/ckanextiati/changeset/e44bbc235c79/
changeset:   e44bbc235c79
branch:      resource-archiver
user:        amercader
date:        2011-11-07 17:05:07
summary:     Adjust log level for some logging calls
affected #:  1 file

diff -r 3b7cdd2e9a4ee006d57a3e6e877efb3354da5b85 -r e44bbc235c79a53af5c22120aa6e9435eb83b52b ckanext/iati/commands.py
--- a/ckanext/iati/commands.py
+++ b/ckanext/iati/commands.py
@@ -72,7 +72,7 @@
 
                 is_activity_package = (package['extras']['filetype'] == 'activity') if 'filetype' in package['extras'] else 'activity'
 
-                log.info('Archiving dataset: %s (%d resources)' % (package.get('name'), len(package.get('resources', []))))
+                log.debug('Archiving dataset: %s (%d resources)' % (package.get('name'), len(package.get('resources', []))))
                 for resource in package.get('resources', []):
 
                     if not resource.get('url',''):
@@ -149,7 +149,7 @@
                     if update:
                         context['id'] = package['id']
                         updated_package = get_action('package_update_rest')(context,package)
-                        log.info('Package %s updated with new extras' % package['name'])
+                        log.debug('Package %s updated with new extras' % package['name'])
                         updated = updated + 1
 
             t2 = datetime.datetime.now()



https://bitbucket.org/okfn/ckanextiati/changeset/248e68ff7544/
changeset:   248e68ff7544
user:        amercader
date:        2011-11-07 19:50:04
summary:     Update labels and helper texts for the forms
affected #:  3 files

diff -r df8a311aa2f07300cdf737b4b41061207b7ee6df -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 ckanext/iati/public/css/iati.css
--- a/ckanext/iati/public/css/iati.css
+++ b/ckanext/iati/public/css/iati.css
@@ -338,3 +338,9 @@
     margin-top: -20px;
     margin-bottom: 20px;
 }
+
+.field-xml{
+    font-weight: normal;
+    font-size: smaller;
+    font-style: italic;
+}


diff -r df8a311aa2f07300cdf737b4b41061207b7ee6df -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 ckanext/iati/templates/group/form_iati.html
--- a/ckanext/iati/templates/group/form_iati.html
+++ b/ckanext/iati/templates/group/form_iati.html
@@ -1,4 +1,4 @@
-<form id="group-edit" action="" method="post" 
+<form id="group-edit" action="" method="post"
     py:attrs="{'class':'has-errors'} if errors else {}"
     xmlns:i18n="http://genshi.edgewall.org/i18n"
     xmlns:py="http://genshi.edgewall.org/"
@@ -11,34 +11,31 @@
             <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li></ul></div>
-    
+
     <fieldset>
-        <legend>Basic information</legend> 
+        <legend>Basic information</legend><dl>
-            <dt><label class="field_req" for="name">Unique Name (required) *</label></dt> 
+            <dt><label class="field_req" for="name">Publisher Id (required) *</label><div class="field-xml">[registry-publisher-id]</div></dt><dd><input id="name" name="name" type="text" value="${data.get('name', '')}" /></dd><dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-            <dd class="instructions basic"><strong>Unique identifier</strong> for group.<br/>2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd> 
-            <script type="text/javascript"> 
-                //<![CDATA[
-                $(document).ready(function () { if (!$('#preview').length) { $("#name").focus();}});
-                //]]>
-            </script>
-            
-            <dt><label class="field_opt" for="title">Title</label></dt> 
+            <dd class="instructions basic"><strong>Unique identifier</strong> for publisher. Where possible use a short abbreviation of your organisation's name. 2+ chars, lowercase, using only 'a-z0-9' and '-_'.</dd>
+
+            <dt><label class="field_opt" for="title">Publisher Name</label></dt><dd><input id="title" name="title" type="text" value="${data.get('title', '')}" /></dd><dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
+            <dd class="instructions basic">The Name of the Organisation publishing the data.</dd>
 
-            <dt><label class="field_req" for="type">Source</label></dt> 
+            <dt><label class="field_req" for="type">Source</label></dt><dd><select id="type" name="type"><py:for each="value, title in c.publisher_source_types"><option value="${value}" py:attrs="{'selected': 'selected' if data.get('type', '') == value else None}" >${title}</option></py:for></select>
-            </dd> 
-            
-            <dt><label class="field_opt" for="license_id">License</label></dt> 
+            </dd>
+            <dd class="instructions basic">Primary - publishers authorised to report their own (or associated organisations') data. Secondary - publishers reporting the activities of other organisations.</dd>
+
+            <dt><label class="field_opt" for="license_id">License</label></dt><dd><select id="license_id" name="license_id"><py:for each="licence_desc, licence_id in c.licences">
@@ -47,8 +44,10 @@
 
                 </select></dd>
+            <dd class="instructions basic">Choose from the dropdown the appropriate licence under which your data is being published. For more information on IATI's licensing guidelines go to <a href="http://iatistandard.org/standard/licencing">http://iatistandard.org/standard/licencing</a>.</dd>
 
-            <dt><label class="field_opt" for="publisher_organization_type">Organization Type</label></dt> 
+
+            <dt><label class="field_opt" for="publisher_organization_type">Organisation Type</label></dt><dd><select id="publisher_organization_type" name="publisher_organization_type"><py:for each="value, title in c.organization_types">
@@ -57,7 +56,7 @@
 
                 </select></dd>
-            <dd class="instructions basic"></dd>
+            <dd class="instructions basic">Choose from the dropdown the organisation type that best describes the publisher.</dd><dt><label class="field_opt" for="publisher_country">Country</label></dt><dd>
@@ -69,13 +68,15 @@
                 </select></py:with></dd>
+            <dd class="instructions basic">Choose from the dropdown the country in which the publisher is legally incorporated. Multilateral and regional organisations may choose a region at the bottom of the list.</dd><dt><label class="field_opt" for="publisher_iati_id">IATI Identifier</label></dt><dd><input id="publisher_iati_id" name="publisher_iati_id" type="text" value="${data.get('publisher_iati_id', '')}" /></dd>
+            <dd class="instructions basic">The organisation identifier used in the IATI-xml files to identify the reporting organisation. (<reporting-org ref="!!!">).</dd><py:choose><py:when test="c.is_sysadmin">
-                    <dt><label class="field_opt" for="state">State</label></dt> 
+                    <dt><label class="field_opt" for="state">State</label></dt><dd><select id="state" name="state"><option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
@@ -83,6 +84,8 @@
                             <option py:attrs="{'selected': 'selected' if data.get('state') == 'pending' else None}" value="pending">pending</option></select></dd>
+                    <dd class="instructions basic">Choose from dropdown: active - publisher record is visibile on the Registry; pending - for editing prior to publication; deleted.</dd>
+
                 </py:when></py:choose>
 
@@ -90,64 +93,64 @@
     </fieldset><fieldset>
-        <legend>Details</legend> 
+        <legend>Details</legend><dl><dt><label class="field_opt" for="publisher_contact">Contact</label></dt>
-            <dd><textarea id="publisher_contact" name="publisher_contact">${data.get('publisher_contact', '')}</textarea></dd> 
-            <dd class="instructions basic">Contact details for publisher</dd>
- 
-            <dt><label class="field_opt" for="publisher_description">Description</label></dt> 
-            <dd><textarea id="publisher_description" name="publisher_description">${data.get('publisher_description', '')}</textarea></dd> 
-            <dd class="instructions basic">General description of publisher's role and activities</dd>
+            <dd><textarea id="publisher_contact" name="publisher_contact">${data.get('publisher_contact', '')}</textarea></dd>
+            <dd class="instructions basic">Contact details for publisher.</dd>
 
-            <dt><label class="field_opt" for="publisher_agencies">Organisations / agencies covered</label></dt> 
-            <dd><textarea id="publisher_agencies" name="publisher_agencies">${data.get('publisher_agencies', '')}</textarea></dd> 
-            <dd class="instructions basic">Whose activities does this publisher publish?</dd>
+            <dt><label class="field_opt" for="publisher_description">Description</label></dt>
+            <dd><textarea id="publisher_description" name="publisher_description">${data.get('publisher_description', '')}</textarea></dd>
+            <dd class="instructions basic">General description of publisher's role and activities.</dd>
 
-            <dt><label class="field_opt" for="publisher_timeliness">Timeliness of Data</label></dt> 
-            <dd><textarea id="publisher_timeliness" name="publisher_timeliness">${data.get('publisher_timeliness','')}</textarea></dd> 
-            <dd class="instructions basic">How up do date is the data when published?</dd> 
+            <dt><label class="field_opt" for="publisher_agencies">Organisations / agencies covered</label></dt>
+            <dd><textarea id="publisher_agencies" name="publisher_agencies">${data.get('publisher_agencies', '')}</textarea></dd>
+            <dd class="instructions basic">Which organisations/agencies does your IATI data cover?  (What % of your total development flows does this cover? What is missing?).</dd>
 
-            <dt><label class="field_opt" for="publisher_frequency">Frequency of publication</label></dt> 
+            <dt><label class="field_opt" for="publisher_timeliness">Timeliness of Data</label></dt>
+            <dd><textarea id="publisher_timeliness" name="publisher_timeliness">${data.get('publisher_timeliness','')}</textarea></dd>
+            <dd class="instructions basic">How soon after data is captured and available internally will data be published?</dd>
+
+            <dt><label class="field_opt" for="publisher_frequency">Frequency of publication</label></dt><dd><textarea id="publisher_frequency" name="publisher_frequency">${data.get('publisher_frequency','')}</textarea></dd><dd class="instructions basic">How often is IATI data refreshed? Monthly/Quarterly?</dd><dt><label class="field_opt" for="publisher_units">Units of Aid</label></dt>
-            <dd><textarea id="publisher_units" name="publisher_units">${data.get('publisher_units','')}</textarea></dd> 
-            <dd class="instructions basic">A description of any hierarchical reporting units used and how they are applied</dd>
+            <dd><textarea id="publisher_units" name="publisher_units">${data.get('publisher_units','')}</textarea></dd>
+            <dd class="instructions basic">How is an activity defined e.g. projects and programmes, or some other structure? Do you have multi-tiered project structures e.g. projects and sub-projects or components? At which level/s do you intend to publish details?).</dd>
 
-            <dt><label class="field_opt" for="publisher_segmentation">Segmentation of Published Data</label></dt> 
+            <dt><label class="field_opt" for="publisher_segmentation">Segmentation of Published Data</label></dt><dd><textarea id="publisher_segmentation" name="publisher_segmentation">${data.get('publisher_segmentation','')}</textarea></dd>
-            <dd class="instructions basic">Is IATI data published by country, regions?</dd>
+            <dd class="instructions basic">Is IATI data published in separate files per country or region?</dd>
 
-            <dt><label class="field_opt" for="publisher_refs">Data Definitions and References</label></dt> 
-            <dd><textarea id="publisher_refs" name="publisher_refs">${data.get('publisher_refs','')}</textarea></dd> 
-            <dd class="instructions basic">Links to guides, explanations, codelists on the publisher's own site that clarify their data</dd> 
+            <dt><label class="field_opt" for="publisher_refs">Data Definitions and References</label></dt>
+            <dd><textarea id="publisher_refs" name="publisher_refs">${data.get('publisher_refs','')}</textarea></dd>
+            <dd class="instructions basic">Links to guides, explanations, codelists on the publisher's own site that clarify their data.</dd>
 
-            <dt><label class="field_opt" for="publisher_field_exclusions">Field Exclusions</label></dt> 
-            <dd><textarea id="publisher_field_exclusions" name="publisher_field_exclusions">${data.get('publisher_field_exclusions','')}</textarea></dd> 
-            <dd class="instructions basic">What fields does the publisher never use - and for what reason</dd> 
+            <dt><label class="field_opt" for="publisher_field_exclusions">Field Exclusions</label></dt>
+            <dd><textarea id="publisher_field_exclusions" name="publisher_field_exclusions">${data.get('publisher_field_exclusions','')}</textarea></dd>
+            <dd class="instructions basic">What fields recommended in the standard do you never use - and for what reason.</dd>
 
-            <dt><label class="field_opt" for="publisher_record_exclusions">Record Exclusions</label></dt> 
-            <dd><textarea id="publisher_record_exclusions" name="publisher_record_exclusions">${data.get('publisher_record_exclusions','')}</textarea></dd> 
-            <dd class="instructions basic">What are the policies for excluding particular activities, or parts of an activity's data?</dd> 
+            <dt><label class="field_opt" for="publisher_record_exclusions">Record Exclusions</label></dt>
+            <dd><textarea id="publisher_record_exclusions" name="publisher_record_exclusions">${data.get('publisher_record_exclusions','')}</textarea></dd>
+            <dd class="instructions basic">What are your policies for excluding particular activities, or parts of an activity's data?</dd>
 
-            <dt><label class="field_opt" for="publisher_thresholds">Thresholds</label></dt> 
-            <dd><textarea id="publisher_thresholds" name="publisher_thresholds">${data.get('publisher_thresholds','')}</textarea></dd> 
-            <dd class="instructions basic">What are the thresholds below which data or whole activities are not published?</dd> 
+            <dt><label class="field_opt" for="publisher_thresholds">Thresholds</label></dt>
+            <dd><textarea id="publisher_thresholds" name="publisher_thresholds">${data.get('publisher_thresholds','')}</textarea></dd>
+            <dd class="instructions basic">What are the thresholds below which data or whole activities are not published?</dd>
 
-            <dt><label class="field_opt" for="publisher_constraints">Other Constraints</label></dt> 
-            <dd><textarea id="publisher_constraints" name="publisher_constraints">${data.get('publisher_constraints','')}</textarea></dd> 
-            <dd class="instructions basic">Other policies that restrict full compliance with the standard</dd> 
-            
-            <dt><label class="field_opt" for="publisher_data_quality">Data Quality</label></dt> 
-            <dd><textarea id="publisher_data_quality" name="publisher_data_quality">${data.get('publisher_data_quality','')}</textarea></dd> 
-            <dd class="instructions basic">Publisher's comment on the status and accuracyof the data - audited/verified, operational/sub to change, etc</dd> 
-             
-            <dt><label class="field_opt" for="publisher_ui">User Interface</label></dt> 
-            <dd><textarea id="publisher_ui" name="publisher_ui">${data.get('publisher_ui','')}</textarea></dd> 
-            <dd class="instructions basic">Link to publisher's own public user activity interface</dd> 
+            <dt><label class="field_opt" for="publisher_constraints">Other Constraints</label></dt>
+            <dd><textarea id="publisher_constraints" name="publisher_constraints">${data.get('publisher_constraints','')}</textarea></dd>
+            <dd class="instructions basic">Other policies or circumstances that restrict your full compliance with the standard.</dd>
+
+            <dt><label class="field_opt" for="publisher_data_quality">Data Quality</label></dt>
+            <dd><textarea id="publisher_data_quality" name="publisher_data_quality">${data.get('publisher_data_quality','')}</textarea></dd>
+            <dd class="instructions basic">Publisher's comment on the status and accuracyof the data - audited/verified, operational/sub to change, etc</dd>
+
+            <dt><label class="field_opt" for="publisher_ui">User Interface</label></dt>
+            <dd><textarea id="publisher_ui" name="publisher_ui">${data.get('publisher_ui','')}</textarea></dd>
+            <dd class="instructions basic">Will IATI data be accessible for end users through an existing or a new user interface on your website?</dd></dl></fieldset>
 
@@ -161,7 +164,7 @@
         </dl><p py:if="not data.get('packages')">There are no records currently in this group.</p></fieldset>
-    
+
     <fieldset><legend>Add records</legend><dl>


diff -r df8a311aa2f07300cdf737b4b41061207b7ee6df -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 ckanext/iati/templates/package/form_iati.html
--- a/ckanext/iati/templates/package/form_iati.html
+++ b/ckanext/iati/templates/package/form_iati.html
@@ -3,7 +3,7 @@
     xmlns:i18n="http://genshi.edgewall.org/i18n"
     xmlns:py="http://genshi.edgewall.org/"
     xmlns:xi="http://www.w3.org/2001/XInclude">
-    
+
     <div class="error-explanation" py:if="error_summary"><h3>Errors in form</h3><p>The form contains invalid entries:</p>
@@ -15,36 +15,28 @@
     <fieldset><legend>Basic information </legend><dl>
-            <dt><label class="field_req" for="name">Name *</label></dt>
+            <dt><label class="field_req" for="name">File Id *</label><div class="field-xml">[registry-file-id]</div></dt><dd><input id="name" name="name" type="text" value="${data.get('name', '')}" /></dd><dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-            <dd class="instructions basic">A unique identifier for the activity record.</dd>
-            <dd class="instructions further">It should be broadly humanly readable, in the spirit of Semantic Web URIs. Only use an acronym if it is widely recognised. Renaming is possible but discouraged.</dd>
-            <dd class="hints">2+ characters, lowercase, using only 'a-z0-9' and '-_'</dd>
-            <script type="text/javascript">
-                //<![CDATA[
-                $(document).ready(function () { if (!$('#preview').length) {$("#name").focus(); } });
-                //]]>
-            </script>
+            <dd class="instructions basic">A unique identifier for the activity record. It must be prefixed with your Publisher Id and a hyphen. It is recommended that you use "org" for your organisation file and the country or region code for segmented activity datasets. 2+ characters, lowercase, using only 'a-z0-9' and '-_' . (eg dfid-ao, wb-org, unops-998).</dd>
 
-            <dt><label class="field_opt" for="title">Title </label></dt>
+            <dt><label class="field_opt" for="title">Title </label><div class="field-xml">[title]</div></dt><dd><input id="title" name="title" type="text" value="${data.get('title', '')}" /></dd><dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
-            <dd class="instructions basic">A short descriptive title for the data set.</dd>
-            <dd class="instructions further">It should not be a description though - save that for the Notes field. Do not give a trailing full stop.</dd>
+            <dd class="instructions basic">The title of the dataset. It should not be a description though - save that for the Notes field. Do not give a trailing full stop.</dd>
 
-            <dt><label class="field_opt" for="author_email">Contact e-mail</label></dt>
+            <dt><label class="field_opt" for="author_email">Contact e-mail</label><div class="field-xml">[contact-email]</div></dt><dd><input id="author_email" name="author_email" type="text" value="${data.get('author_email', '')}" /></dd>
+            <dd class="instructions basic">A contact address for anyone wishing to query you about your data.</dd>
 
-            <dt><label class="field_opt" for="state">File type</label></dt>
+            <dt><label class="field_opt" for="state">File type</label><div class="field-xml">[file-type]</div></dt><dd><select id="filetype" name="filetype"><option py:attrs="{'selected': 'selected' if data.get('filetype') == 'activity' else None}" value="activity">Activity</option><option py:attrs="{'selected': 'selected' if data.get('filetype') == 'organisation' else None}" value="organisation">Organisation</option></select></dd>
-
-
+            <dd class="instructions basic">Choose from dropdown. Either "Activity" or "Organisation".</dd></dl></fieldset>
 
@@ -76,6 +68,7 @@
             </select></dd><dd py:if="not c.groups_available">Cannot add any publisher.</dd>
+            <dd class="instructions basic">Choose your own Publisher Name from the Dropdown. Your Publisher record needs to have been created and authorised by the Registry administrator before you can publish datsets.</dd></dl></fieldset>
@@ -83,7 +76,7 @@
     <fieldset><legend>Details</legend><dl>
-            <dt><label class="field_opt" for="country">Recipient country</label></dt>
+            <dt><label class="field_opt" for="country">Recipient country</label><div class="field-xml">[recipient-country]</div></dt><dd><py:with vars="country = data.get('country','')"><select id="country" name="country">
@@ -93,18 +86,19 @@
                 </select></py:with></dd>
+            <dd class="instructions basic">Select the country or region (listed below the countries in the dropdown).</dd>
 
-            <dt><label class="field_opt" for="record_updated">Record updated</label></dt>
+            <dt><label class="field_opt" for="record_updated">Record updated</label><div class="field-xml">[generated-datetime]</div></dt><dd><input id="record_updated" name="record_updated" size="40" type="text" value="${data.get('record_updated', '')}" /></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The date on which this metadata was last updated. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="data_updated">Data updated</label></dt>
+            <dt><label class="field_opt" for="data_updated">Data updated</label><div class="field-xml">[last-updated-datetime]</div></dt><dd><input id="data_updated" name="data_updated" size="40" type="text" value="${data.get('data_updated', '')}"/></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The date on which the linked file was last updated. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="record_updated">Language</label></dt>
+            <dt><label class="field_opt" for="record_updated">Language</label><div class="field-xml">[default-language]</div></dt><dd><input id="language" name="language" size="40" type="text" value="${data.get('language', '')}" /></dd>
-            <dd class="instructions basic">The ISO 639-1 code for the language of the file (e.g. 'en')</dd>
+            <dd class="instructions basic">The ISO 639-1 code for the default language of the file (e.g. 'en').</dd><dt><label class="field_opt" for="license_id">License</label></dt><dd>
@@ -114,7 +108,7 @@
                     </py:for></select></dd>
-            <dd class="instructions basic">The licence under which the dataset is released.</dd>
+            <dd class="instructions basic">Choose from the dropdown the appropriate licence under which your data is being published. This will  already contain the licence specified in your publisher record, For more information on IATI's licensing guidelines go to <a href="http://iatistandard.org/standard/licencing">http://iatistandard.org/standard/licencing</a>.</dd><dt><label class="field_opt" for="tags">Tags</label></dt><dd>
@@ -163,28 +157,29 @@
         </table><div class="instructions basic">The files containing the data or address of the APIs for accessing it.</div>
-        <div class="instructions further"><br /><b>URL:</b> This is the Internet link directly to the data - by selecting this link in a web browser, the user will immediately download the full dataset. Note that datasets are not hosted on this site, but by the publisher of the data.<br /><b>Format:</b> This should give the file format in which the data is supplied. (i.e. IATI-XML)<br /><b>Description</b> Any information you want to add to describe the resource.<br /></div>
+        <div class="instructions further"><br /><b>URL:</b><span class="field-xml">[source-url]</span> This is the Internet link directly to the data - by selecting this link in a web browser, the user will immediately download the full dataset. Note that datasets are not hosted on this site, but by the publisher of the data.<br /><b>Format:</b><span class="field-xml">[format]</span>This should give the file format in which the data is supplied. IATI-compliant data should specify "IATI-XML".<br /><b>Description</b> Any information you want to add to describe the resource.<br /></div><div class="field_error" py:if="errors.get('resources', '')">Dataset resource(s) incomplete.</div></fieldset><fieldset><legend>Verification and Analysis </legend><dl>
-            <dt><label class="field_opt" for="activity_period">Activitiy Period</label></dt>
+            <dt><label class="field_opt" for="activity_period">Activitiy Period</label><div class="field-xml">[activiy-period-start -<br/>activity-period-end]</div></dt><dd><input class="short" id="activity_period-from" name="activity_period-from" type="text" value="${data.get('activity_period-from', '')}" /> -
                 <input class="short" id="activity_period-to" name="activity_period-to" type="text" value="${data.get('activity_period-to', '')}" /></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The earliest and latest dates reported for the starting and ending of activities within this dataset. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="activity_count">Num. Activities</label></dt>
+            <dt><label class="field_opt" for="activity_count">Num. Activities</label><div class="field-xml">[activity-count]</div></dt><dd><input id="activity_count" name="activity_count" size="40" type="text" value="${data.get('activity_count', '')}"/></dd>
+            <dd class="instructions basic">A count of the number of activities reported.</dd><dt><label class="field_opt" for="archive_file">Archive</label></dt><dd><input id="archive_file" name="archive_file" size="40" type="checkbox" py:attrs="{'checked': 'checked' if data.get('archive_file','') == 'yes' else None}" /></dd><py:choose><py:when test="c.is_sysadmin">
-                <dt><label class="field_opt" for="verified">Verification</label></dt>
+                <dt><label class="field_opt" for="verified">Verification</label><div class="field-xml">[verification-status]</div></dt><dd><input id="verified" name="verified" size="40" type="checkbox" py:attrs="{'checked': 'checked' if data.get('verified','') == 'yes' else None}" /></dd><dt><label class="field_opt" for="state">State</label></dt>
@@ -197,7 +192,7 @@
                 </dd></py:when></py:choose>
-
+            <dd class="instructions basic">Choose from the dropdown: "active" - published; "pending" - pre-publication; "deleted"</dd></dl></fieldset>
 



https://bitbucket.org/okfn/ckanextiati/changeset/a10ddd8d6c22/
changeset:   a10ddd8d6c22
branch:      spreadsheet-support
user:        amercader
date:        2011-11-07 19:52:18
summary:     [merge] from resource-archiver
affected #:  5 files

diff -r af188d2894d8a82681b83b57cf85e63c822bb1da -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 README.txt
--- a/README.txt
+++ b/README.txt
@@ -23,13 +23,10 @@
 1) Install the extension from its source repository (a Debian package 
 is not available at this time):
 
-(env)$ pip install -e hg+https://okfn@bitbucket.org/okfn/ckanext-solr#egg=ckanext-solr
 (env)$ pip install -e hg+https://okfn@bitbucket.org/okfn/ckanextiati#egg=ckanextiati
 
-(Solr is a requirement but also available in packaged form).
-
-You probably also want to install the ckanext-solr and
-ckanext-wordpresser packages.  See their respective documentation for
+You probably also want to install the ckanext-wordpresser and
+ckanext-archiver packages.  See their respective documentation for
 install notes.
 
 2) Copy or symlink the modified Solr schema.xml into the Solr core 
@@ -63,6 +60,10 @@
 
 (Don't forget to also add a 'solr_url').
 
+# User credentials used in the archiver
+iati.admin_user.name = <user_name>
+iati.admin_user.api_key = <api_key>
+ 
 
 Overall workflow for IATI
 =========================
@@ -123,3 +124,12 @@
 report data itself. 
 
 
+Archiver command
+================
+
+The extension includes a paster command that will download all IATI XML files
+(i.e. all resources), parse them and extract a couple of variables, which will
+be stored in extras. To run it you must install ckanext-archiver. To run the
+command, assuming you are on the ckanextiati directory::
+
+    paster iati-archiver update --config=../ckan/development.ini


diff -r af188d2894d8a82681b83b57cf85e63c822bb1da -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 ckanext/iati/commands.py
--- /dev/null
+++ b/ckanext/iati/commands.py
@@ -0,0 +1,199 @@
+import os
+import datetime
+from lxml import etree
+import requests
+import json
+from pylons import config
+from ckan.lib.cli import CkanCommand
+from ckan.logic import get_action
+from ckan import model
+
+from ckan.lib.helpers import date_str_to_datetime
+from ckanext.archiver import tasks
+import logging
+
+log = logging.getLogger('iati_archiver')
+
+class Archiver(CkanCommand):
+    '''
+    Download and save copies of all IATI activity files, extract some metrics
+    from them and store them as extras.
+
+    Usage:
+
+        paster iati-archiver update [{package-id}]
+           - Archive all activity files or just those belonging to a specific package
+             if a package id is provided
+
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+    min_args = 0
+    max_args = 2
+    pkg_names = []
+
+    def command(self):
+        '''
+        Parse command line arguments and call appropriate method.
+        '''
+        if not self.args or self.args[0] in ['--help', '-h', 'help']:
+            print Archiver.__doc__
+            return
+
+        t1 = datetime.datetime.now()
+
+        cmd = self.args[0]
+        self._load_config()
+        # TODO: use this when it gets to default ckan
+        # username = get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+        context = {
+            'model': model,
+            'session':model.Session,
+            'site_url':config.get('ckan.site_url'),
+            'user': config.get('iati.admin_user.name'),
+            'apikey': config.get('iati.admin_user.api_key')
+        }
+
+        if cmd == 'update':
+            if len(self.args) > 1:
+                packages = [unicode(self.args[1])]
+
+            else:
+                packages = get_action('package_list')(context, {})
+
+            data_formats = tasks.DATA_FORMATS
+            data_formats.append('iati-xml')
+
+            log.info('IATI Archiver: starting  %s' % str(t1))
+            log.info('Number of datasets to archive: %d' % len(packages))
+            updated = 0
+            for package_id in packages:
+                package = get_action('package_show_rest')(context,{'id': package_id})
+
+                is_activity_package = (package['extras']['filetype'] == 'activity') if 'filetype' in package['extras'] else 'activity'
+
+                log.debug('Archiving dataset: %s (%d resources)' % (package.get('name'), len(package.get('resources', []))))
+                for resource in package.get('resources', []):
+
+                    if not resource.get('url',''):
+                        log.error('Resource for dataset %s does not have URL' % package['name'])
+                        continue
+
+                    try:
+                        result = tasks.download(context,resource,data_formats=data_formats)
+                    except tasks.LinkCheckerError,e:
+                        if 'method not allowed' in str(e).lower():
+                            # The DFID server does not support HEAD requests*,
+                            # so we need to handle the download manually
+                            # * But only the first time a file is downloaded!?
+                            result = _download_resource(context,resource,data_formats=data_formats)
+                        else:
+                            log.error('Invalid resource URL for dataset %s: %s' % (package['name'],str(e)))
+                            continue
+                    except tasks.DownloadError,e:
+                        log.error('Error downloading resource for dataset %s: %s' % (package['name'],str(e)))
+                        continue
+
+                    if 'zip' in result['headers']['content-type']:
+                        # Skip zipped files for now
+                        log.info('Skipping zipped file for dataset %s ' % package.get('name'))
+                        continue
+
+                    file_path = result['saved_file']
+                    f = open(file_path,'r')
+                    xml = f.read()
+                    f.close()
+                    os.remove(file_path)
+                    try:
+                        tree = etree.fromstring(xml)
+                    except etree.XMLSyntaxError,e:
+                        log.error('Could not parse XML file for dataset %s: %s' % (package['name'],str(e)))
+                        continue
+
+                    new_extras = {}
+                    if is_activity_package:
+                        # Number of activities (activity_count extra)
+                        new_extras['activity_count'] = int(tree.xpath('count(//iati-activity)'))
+
+                    # Last updated date (data_updated extra)
+                    if is_activity_package:
+                        xpath = 'iati-activity/@last-updated-datetime'
+                    else:
+                        xpath = 'iati-organisation/@last-updated-datetime'
+                    dates = tree.xpath(xpath) or []
+
+                    sorted(dates,reverse=True)
+                    last_updated_date = dates[0] if len(dates) else None
+
+                    # Check dates
+                    if last_updated_date:
+                        # Get rid of the microseconds
+                        if '.' in last_updated_date:
+                            last_updated_date = last_updated_date[:last_updated_date.find('.')]
+                        try:
+                            date = date_str_to_datetime(last_updated_date)
+                            format = '%Y-%m-%d %H:%M' if (date.hour and date.minute) else '%Y-%m-%d'
+                            new_extras['data_updated'] = date.strftime(format)
+                        except (ValueError,TypeError),e:
+                            log.error('Wrong date format for data_updated: %s' % str(e))
+
+
+                    update = False
+                    for key,value in new_extras.iteritems():
+                        if value and (not key in package['extras'] or value != package['extras'][key]):
+                            update = True
+                            old_value = package['extras'][key] if key in package['extras'] else '""'
+                            log.info('Updated extra %s for dataset %s: %s -> %s' % (key,package['name'],old_value,value))
+                            package['extras'][key] = value
+
+                    if update:
+                        context['id'] = package['id']
+                        updated_package = get_action('package_update_rest')(context,package)
+                        log.debug('Package %s updated with new extras' % package['name'])
+                        updated = updated + 1
+
+            t2 = datetime.datetime.now()
+
+            log.info('IATI Archiver: Done. Updated %i packages. Total time: %s' % (updated,str(t2 - t1)))
+        else:
+            log.error('Command %s not recognized' % (cmd,))
+
+def _download_resource(context,resource, max_content_length=50000000, url_timeout=30,data_formats=['xml','iati-xml']):
+
+    # get the resource and archive it
+    #logger.info("Resource identified as data file, attempting to archive")
+    res = requests.get(resource['url'], timeout = url_timeout)
+
+    headers = res.headers
+    resource_format = resource['format'].lower()
+    ct = headers.get('content-type', '').lower()
+    cl = headers.get('content-length')
+
+    resource_changed = (resource.get('mimetype') != ct) or (resource.get('size') != cl)
+    if resource_changed:
+        resource['mimetype'] = ct
+        resource['size'] = cl
+
+    length, hash, saved_file = tasks._save_resource(resource, res, max_content_length)
+
+    # check that resource did not exceed maximum size when being saved
+    # (content-length header could have been invalid/corrupted, or not accurate
+    # if resource was streamed)
+    #
+    # TODO: remove partially archived file in this case
+    if length >= max_content_length:
+        if resource_changed:
+            tasks._update_resource(context, resource)
+        # record fact that resource is too large to archive
+        raise tasks.DownloadError("Content-length after streaming reached maximum allowed value of %s" %
+            max_content_length)
+
+    # update the resource metadata in CKAN
+    resource['hash'] = hash
+    tasks._update_resource(context, resource)
+
+    return {'length': length,
+            'hash' :hash,
+            'headers': headers,
+            'saved_file': saved_file}
+


diff -r af188d2894d8a82681b83b57cf85e63c822bb1da -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 ckanext/iati/controllers/package_iati.py
--- a/ckanext/iati/controllers/package_iati.py
+++ b/ckanext/iati/controllers/package_iati.py
@@ -66,6 +66,12 @@
             'verified': [convert_from_extras,ignore_missing],
             'language': [convert_from_extras, ignore_missing],
         })
+        # Remove isodate validator
+        schema['resources'].update({
+            'last_modified': [ignore_missing],
+            'cache_last_updated': [ignore_missing],
+            'webstore_last_updated': [ignore_missing]
+        })
 
         return schema
 


diff -r af188d2894d8a82681b83b57cf85e63c822bb1da -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 setup.py
--- a/setup.py
+++ b/setup.py
@@ -35,6 +35,6 @@
       
       [paste.paster_command]
       create-iati-fixtures = ckanext.iati.fixtures:CreateIatiFixtures
-      
+      iati-archiver=ckanext.iati.commands:Archiver
       """,
       )


diff -r af188d2894d8a82681b83b57cf85e63c822bb1da -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 solr/schema.xml
--- a/solr/schema.xml
+++ b/solr/schema.xml
@@ -91,6 +91,7 @@
 
 
  <fields>
+   <field name="index_id" type="string" indexed="true" stored="true" required="true" /><field name="id" type="string" indexed="true" stored="true" required="true" /><field name="site_id" type="string" indexed="true" stored="true" required="true" /><field name="title" type="text" indexed="true" stored="true" />
@@ -138,7 +139,7 @@
    <dynamicField name="*" type="string" indexed="true"  stored="false"/></fields>
 
- <uniqueKey>id</uniqueKey>
+ <uniqueKey>index_id</uniqueKey><defaultSearchField>text</defaultSearchField><solrQueryParser defaultOperator="AND"/>
 



https://bitbucket.org/okfn/ckanextiati/changeset/3059a06a288c/
changeset:   3059a06a288c
branch:      spreadsheet-support
user:        amercader
date:        2011-11-07 19:55:14
summary:     [merge] from default
affected #:  4 files

diff -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 -r 3059a06a288cb5e7bcbe58704cc9e93326045def ckanext/iati/public/css/iati.css
--- a/ckanext/iati/public/css/iati.css
+++ b/ckanext/iati/public/css/iati.css
@@ -338,3 +338,9 @@
     margin-top: -20px;
     margin-bottom: 20px;
 }
+
+.field-xml{
+    font-weight: normal;
+    font-size: smaller;
+    font-style: italic;
+}


diff -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 -r 3059a06a288cb5e7bcbe58704cc9e93326045def ckanext/iati/public/css/overrides.css
--- a/ckanext/iati/public/css/overrides.css
+++ b/ckanext/iati/public/css/overrides.css
@@ -588,3 +588,7 @@
     list-style-type: circle
 }
 
+.entry-content ol {
+    margin-left: 4em;
+    font-size: 12px;
+}


diff -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 -r 3059a06a288cb5e7bcbe58704cc9e93326045def ckanext/iati/templates/group/form_iati.html
--- a/ckanext/iati/templates/group/form_iati.html
+++ b/ckanext/iati/templates/group/form_iati.html
@@ -1,4 +1,4 @@
-<form id="group-edit" action="" method="post" 
+<form id="group-edit" action="" method="post"
     py:attrs="{'class':'has-errors'} if errors else {}"
     xmlns:i18n="http://genshi.edgewall.org/i18n"
     xmlns:py="http://genshi.edgewall.org/"
@@ -11,34 +11,31 @@
             <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li></ul></div>
-    
+
     <fieldset>
-        <legend>Basic information</legend> 
+        <legend>Basic information</legend><dl>
-            <dt><label class="field_req" for="name">Unique Name (required) *</label></dt> 
+            <dt><label class="field_req" for="name">Publisher Id (required) *</label><div class="field-xml">[registry-publisher-id]</div></dt><dd><input id="name" name="name" type="text" value="${data.get('name', '')}" /></dd><dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-            <dd class="instructions basic"><strong>Unique identifier</strong> for group.<br/>2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd> 
-            <script type="text/javascript"> 
-                //<![CDATA[
-                $(document).ready(function () { if (!$('#preview').length) { $("#name").focus();}});
-                //]]>
-            </script>
-            
-            <dt><label class="field_opt" for="title">Title</label></dt> 
+            <dd class="instructions basic"><strong>Unique identifier</strong> for publisher. Where possible use a short abbreviation of your organisation's name. 2+ chars, lowercase, using only 'a-z0-9' and '-_'.</dd>
+
+            <dt><label class="field_opt" for="title">Publisher Name</label></dt><dd><input id="title" name="title" type="text" value="${data.get('title', '')}" /></dd><dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
+            <dd class="instructions basic">The Name of the Organisation publishing the data.</dd>
 
-            <dt><label class="field_req" for="type">Source</label></dt> 
+            <dt><label class="field_req" for="type">Source</label></dt><dd><select id="type" name="type"><py:for each="value, title in c.publisher_source_types"><option value="${value}" py:attrs="{'selected': 'selected' if data.get('type', '') == value else None}" >${title}</option></py:for></select>
-            </dd> 
-            
-            <dt><label class="field_opt" for="license_id">License</label></dt> 
+            </dd>
+            <dd class="instructions basic">Primary - publishers authorised to report their own (or associated organisations') data. Secondary - publishers reporting the activities of other organisations.</dd>
+
+            <dt><label class="field_opt" for="license_id">License</label></dt><dd><select id="license_id" name="license_id"><py:for each="licence_desc, licence_id in c.licences">
@@ -47,8 +44,10 @@
 
                 </select></dd>
+            <dd class="instructions basic">Choose from the dropdown the appropriate licence under which your data is being published. For more information on IATI's licensing guidelines go to <a href="http://iatistandard.org/standard/licencing">http://iatistandard.org/standard/licencing</a>.</dd>
 
-            <dt><label class="field_opt" for="publisher_organization_type">Organization Type</label></dt> 
+
+            <dt><label class="field_opt" for="publisher_organization_type">Organisation Type</label></dt><dd><select id="publisher_organization_type" name="publisher_organization_type"><py:for each="value, title in c.organization_types">
@@ -57,7 +56,7 @@
 
                 </select></dd>
-            <dd class="instructions basic"></dd>
+            <dd class="instructions basic">Choose from the dropdown the organisation type that best describes the publisher.</dd><dt><label class="field_opt" for="publisher_country">Country</label></dt><dd>
@@ -69,13 +68,15 @@
                 </select></py:with></dd>
+            <dd class="instructions basic">Choose from the dropdown the country in which the publisher is legally incorporated. Multilateral and regional organisations may choose a region at the bottom of the list.</dd><dt><label class="field_opt" for="publisher_iati_id">IATI Identifier</label></dt><dd><input id="publisher_iati_id" name="publisher_iati_id" type="text" value="${data.get('publisher_iati_id', '')}" /></dd>
+            <dd class="instructions basic">The organisation identifier used in the IATI-xml files to identify the reporting organisation. (<reporting-org ref="!!!">).</dd><py:choose><py:when test="c.is_sysadmin">
-                    <dt><label class="field_opt" for="state">State</label></dt> 
+                    <dt><label class="field_opt" for="state">State</label></dt><dd><select id="state" name="state"><option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
@@ -83,6 +84,8 @@
                             <option py:attrs="{'selected': 'selected' if data.get('state') == 'pending' else None}" value="pending">pending</option></select></dd>
+                    <dd class="instructions basic">Choose from dropdown: active - publisher record is visibile on the Registry; pending - for editing prior to publication; deleted.</dd>
+
                 </py:when></py:choose>
 
@@ -90,64 +93,64 @@
     </fieldset><fieldset>
-        <legend>Details</legend> 
+        <legend>Details</legend><dl><dt><label class="field_opt" for="publisher_contact">Contact</label></dt>
-            <dd><textarea id="publisher_contact" name="publisher_contact">${data.get('publisher_contact', '')}</textarea></dd> 
-            <dd class="instructions basic">Contact details for publisher</dd>
- 
-            <dt><label class="field_opt" for="publisher_description">Description</label></dt> 
-            <dd><textarea id="publisher_description" name="publisher_description">${data.get('publisher_description', '')}</textarea></dd> 
-            <dd class="instructions basic">General description of publisher's role and activities</dd>
+            <dd><textarea id="publisher_contact" name="publisher_contact">${data.get('publisher_contact', '')}</textarea></dd>
+            <dd class="instructions basic">Contact details for publisher.</dd>
 
-            <dt><label class="field_opt" for="publisher_agencies">Organisations / agencies covered</label></dt> 
-            <dd><textarea id="publisher_agencies" name="publisher_agencies">${data.get('publisher_agencies', '')}</textarea></dd> 
-            <dd class="instructions basic">Whose activities does this publisher publish?</dd>
+            <dt><label class="field_opt" for="publisher_description">Description</label></dt>
+            <dd><textarea id="publisher_description" name="publisher_description">${data.get('publisher_description', '')}</textarea></dd>
+            <dd class="instructions basic">General description of publisher's role and activities.</dd>
 
-            <dt><label class="field_opt" for="publisher_timeliness">Timeliness of Data</label></dt> 
-            <dd><textarea id="publisher_timeliness" name="publisher_timeliness">${data.get('publisher_timeliness','')}</textarea></dd> 
-            <dd class="instructions basic">How up do date is the data when published?</dd> 
+            <dt><label class="field_opt" for="publisher_agencies">Organisations / agencies covered</label></dt>
+            <dd><textarea id="publisher_agencies" name="publisher_agencies">${data.get('publisher_agencies', '')}</textarea></dd>
+            <dd class="instructions basic">Which organisations/agencies does your IATI data cover?  (What % of your total development flows does this cover? What is missing?).</dd>
 
-            <dt><label class="field_opt" for="publisher_frequency">Frequency of publication</label></dt> 
+            <dt><label class="field_opt" for="publisher_timeliness">Timeliness of Data</label></dt>
+            <dd><textarea id="publisher_timeliness" name="publisher_timeliness">${data.get('publisher_timeliness','')}</textarea></dd>
+            <dd class="instructions basic">How soon after data is captured and available internally will data be published?</dd>
+
+            <dt><label class="field_opt" for="publisher_frequency">Frequency of publication</label></dt><dd><textarea id="publisher_frequency" name="publisher_frequency">${data.get('publisher_frequency','')}</textarea></dd><dd class="instructions basic">How often is IATI data refreshed? Monthly/Quarterly?</dd><dt><label class="field_opt" for="publisher_units">Units of Aid</label></dt>
-            <dd><textarea id="publisher_units" name="publisher_units">${data.get('publisher_units','')}</textarea></dd> 
-            <dd class="instructions basic">A description of any hierarchical reporting units used and how they are applied</dd>
+            <dd><textarea id="publisher_units" name="publisher_units">${data.get('publisher_units','')}</textarea></dd>
+            <dd class="instructions basic">How is an activity defined e.g. projects and programmes, or some other structure? Do you have multi-tiered project structures e.g. projects and sub-projects or components? At which level/s do you intend to publish details?).</dd>
 
-            <dt><label class="field_opt" for="publisher_segmentation">Segmentation of Published Data</label></dt> 
+            <dt><label class="field_opt" for="publisher_segmentation">Segmentation of Published Data</label></dt><dd><textarea id="publisher_segmentation" name="publisher_segmentation">${data.get('publisher_segmentation','')}</textarea></dd>
-            <dd class="instructions basic">Is IATI data published by country, regions?</dd>
+            <dd class="instructions basic">Is IATI data published in separate files per country or region?</dd>
 
-            <dt><label class="field_opt" for="publisher_refs">Data Definitions and References</label></dt> 
-            <dd><textarea id="publisher_refs" name="publisher_refs">${data.get('publisher_refs','')}</textarea></dd> 
-            <dd class="instructions basic">Links to guides, explanations, codelists on the publisher's own site that clarify their data</dd> 
+            <dt><label class="field_opt" for="publisher_refs">Data Definitions and References</label></dt>
+            <dd><textarea id="publisher_refs" name="publisher_refs">${data.get('publisher_refs','')}</textarea></dd>
+            <dd class="instructions basic">Links to guides, explanations, codelists on the publisher's own site that clarify their data.</dd>
 
-            <dt><label class="field_opt" for="publisher_field_exclusions">Field Exclusions</label></dt> 
-            <dd><textarea id="publisher_field_exclusions" name="publisher_field_exclusions">${data.get('publisher_field_exclusions','')}</textarea></dd> 
-            <dd class="instructions basic">What fields does the publisher never use - and for what reason</dd> 
+            <dt><label class="field_opt" for="publisher_field_exclusions">Field Exclusions</label></dt>
+            <dd><textarea id="publisher_field_exclusions" name="publisher_field_exclusions">${data.get('publisher_field_exclusions','')}</textarea></dd>
+            <dd class="instructions basic">What fields recommended in the standard do you never use - and for what reason.</dd>
 
-            <dt><label class="field_opt" for="publisher_record_exclusions">Record Exclusions</label></dt> 
-            <dd><textarea id="publisher_record_exclusions" name="publisher_record_exclusions">${data.get('publisher_record_exclusions','')}</textarea></dd> 
-            <dd class="instructions basic">What are the policies for excluding particular activities, or parts of an activity's data?</dd> 
+            <dt><label class="field_opt" for="publisher_record_exclusions">Record Exclusions</label></dt>
+            <dd><textarea id="publisher_record_exclusions" name="publisher_record_exclusions">${data.get('publisher_record_exclusions','')}</textarea></dd>
+            <dd class="instructions basic">What are your policies for excluding particular activities, or parts of an activity's data?</dd>
 
-            <dt><label class="field_opt" for="publisher_thresholds">Thresholds</label></dt> 
-            <dd><textarea id="publisher_thresholds" name="publisher_thresholds">${data.get('publisher_thresholds','')}</textarea></dd> 
-            <dd class="instructions basic">What are the thresholds below which data or whole activities are not published?</dd> 
+            <dt><label class="field_opt" for="publisher_thresholds">Thresholds</label></dt>
+            <dd><textarea id="publisher_thresholds" name="publisher_thresholds">${data.get('publisher_thresholds','')}</textarea></dd>
+            <dd class="instructions basic">What are the thresholds below which data or whole activities are not published?</dd>
 
-            <dt><label class="field_opt" for="publisher_constraints">Other Constraints</label></dt> 
-            <dd><textarea id="publisher_constraints" name="publisher_constraints">${data.get('publisher_constraints','')}</textarea></dd> 
-            <dd class="instructions basic">Other policies that restrict full compliance with the standard</dd> 
-            
-            <dt><label class="field_opt" for="publisher_data_quality">Data Quality</label></dt> 
-            <dd><textarea id="publisher_data_quality" name="publisher_data_quality">${data.get('publisher_data_quality','')}</textarea></dd> 
-            <dd class="instructions basic">Publisher's comment on the status and accuracyof the data - audited/verified, operational/sub to change, etc</dd> 
-             
-            <dt><label class="field_opt" for="publisher_ui">User Interface</label></dt> 
-            <dd><textarea id="publisher_ui" name="publisher_ui">${data.get('publisher_ui','')}</textarea></dd> 
-            <dd class="instructions basic">Link to publisher's own public user activity interface</dd> 
+            <dt><label class="field_opt" for="publisher_constraints">Other Constraints</label></dt>
+            <dd><textarea id="publisher_constraints" name="publisher_constraints">${data.get('publisher_constraints','')}</textarea></dd>
+            <dd class="instructions basic">Other policies or circumstances that restrict your full compliance with the standard.</dd>
+
+            <dt><label class="field_opt" for="publisher_data_quality">Data Quality</label></dt>
+            <dd><textarea id="publisher_data_quality" name="publisher_data_quality">${data.get('publisher_data_quality','')}</textarea></dd>
+            <dd class="instructions basic">Publisher's comment on the status and accuracyof the data - audited/verified, operational/sub to change, etc</dd>
+
+            <dt><label class="field_opt" for="publisher_ui">User Interface</label></dt>
+            <dd><textarea id="publisher_ui" name="publisher_ui">${data.get('publisher_ui','')}</textarea></dd>
+            <dd class="instructions basic">Will IATI data be accessible for end users through an existing or a new user interface on your website?</dd></dl></fieldset>
 
@@ -161,7 +164,7 @@
         </dl><p py:if="not data.get('packages')">There are no records currently in this group.</p></fieldset>
-    
+
     <fieldset><legend>Add records</legend><dl>


diff -r a10ddd8d6c221beb5744d25ef6c7c631a28e7b94 -r 3059a06a288cb5e7bcbe58704cc9e93326045def ckanext/iati/templates/package/form_iati.html
--- a/ckanext/iati/templates/package/form_iati.html
+++ b/ckanext/iati/templates/package/form_iati.html
@@ -3,7 +3,7 @@
     xmlns:i18n="http://genshi.edgewall.org/i18n"
     xmlns:py="http://genshi.edgewall.org/"
     xmlns:xi="http://www.w3.org/2001/XInclude">
-    
+
     <div class="error-explanation" py:if="error_summary"><h3>Errors in form</h3><p>The form contains invalid entries:</p>
@@ -15,36 +15,28 @@
     <fieldset><legend>Basic information </legend><dl>
-            <dt><label class="field_req" for="name">Name *</label></dt>
+            <dt><label class="field_req" for="name">File Id *</label><div class="field-xml">[registry-file-id]</div></dt><dd><input id="name" name="name" type="text" value="${data.get('name', '')}" /></dd><dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-            <dd class="instructions basic">A unique identifier for the activity record.</dd>
-            <dd class="instructions further">It should be broadly humanly readable, in the spirit of Semantic Web URIs. Only use an acronym if it is widely recognised. Renaming is possible but discouraged.</dd>
-            <dd class="hints">2+ characters, lowercase, using only 'a-z0-9' and '-_'</dd>
-            <script type="text/javascript">
-                //<![CDATA[
-                $(document).ready(function () { if (!$('#preview').length) {$("#name").focus(); } });
-                //]]>
-            </script>
+            <dd class="instructions basic">A unique identifier for the activity record. It must be prefixed with your Publisher Id and a hyphen. It is recommended that you use "org" for your organisation file and the country or region code for segmented activity datasets. 2+ characters, lowercase, using only 'a-z0-9' and '-_' . (eg dfid-ao, wb-org, unops-998).</dd>
 
-            <dt><label class="field_opt" for="title">Title </label></dt>
+            <dt><label class="field_opt" for="title">Title </label><div class="field-xml">[title]</div></dt><dd><input id="title" name="title" type="text" value="${data.get('title', '')}" /></dd><dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
-            <dd class="instructions basic">A short descriptive title for the data set.</dd>
-            <dd class="instructions further">It should not be a description though - save that for the Notes field. Do not give a trailing full stop.</dd>
+            <dd class="instructions basic">The title of the dataset. It should not be a description though - save that for the Notes field. Do not give a trailing full stop.</dd>
 
-            <dt><label class="field_opt" for="author_email">Contact e-mail</label></dt>
+            <dt><label class="field_opt" for="author_email">Contact e-mail</label><div class="field-xml">[contact-email]</div></dt><dd><input id="author_email" name="author_email" type="text" value="${data.get('author_email', '')}" /></dd>
+            <dd class="instructions basic">A contact address for anyone wishing to query you about your data.</dd>
 
-            <dt><label class="field_opt" for="state">File type</label></dt>
+            <dt><label class="field_opt" for="state">File type</label><div class="field-xml">[file-type]</div></dt><dd><select id="filetype" name="filetype"><option py:attrs="{'selected': 'selected' if data.get('filetype') == 'activity' else None}" value="activity">Activity</option><option py:attrs="{'selected': 'selected' if data.get('filetype') == 'organisation' else None}" value="organisation">Organisation</option></select></dd>
-
-
+            <dd class="instructions basic">Choose from dropdown. Either "Activity" or "Organisation".</dd></dl></fieldset>
 
@@ -77,6 +69,7 @@
             </select></dd><dd py:if="not c.groups_available">Cannot add any publisher.</dd>
+            <dd class="instructions basic">Choose your own Publisher Name from the Dropdown. Your Publisher record needs to have been created and authorised by the Registry administrator before you can publish datsets.</dd></dl></fieldset>
@@ -84,7 +77,7 @@
     <fieldset><legend>Details</legend><dl>
-            <dt><label class="field_opt" for="country">Recipient country</label></dt>
+            <dt><label class="field_opt" for="country">Recipient country</label><div class="field-xml">[recipient-country]</div></dt><dd><py:with vars="country = data.get('country','')"><select id="country" name="country">
@@ -94,18 +87,19 @@
                 </select></py:with></dd>
+            <dd class="instructions basic">Select the country or region (listed below the countries in the dropdown).</dd>
 
-            <dt><label class="field_opt" for="record_updated">Record updated</label></dt>
+            <dt><label class="field_opt" for="record_updated">Record updated</label><div class="field-xml">[generated-datetime]</div></dt><dd><input id="record_updated" name="record_updated" size="40" type="text" value="${data.get('record_updated', '')}" /></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The date on which this metadata was last updated. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="data_updated">Data updated</label></dt>
+            <dt><label class="field_opt" for="data_updated">Data updated</label><div class="field-xml">[last-updated-datetime]</div></dt><dd><input id="data_updated" name="data_updated" size="40" type="text" value="${data.get('data_updated', '')}"/></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The date on which the linked file was last updated. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="record_updated">Language</label></dt>
+            <dt><label class="field_opt" for="record_updated">Language</label><div class="field-xml">[default-language]</div></dt><dd><input id="language" name="language" size="40" type="text" value="${data.get('language', '')}" /></dd>
-            <dd class="instructions basic">The ISO 639-1 code for the language of the file (e.g. 'en')</dd>
+            <dd class="instructions basic">The ISO 639-1 code for the default language of the file (e.g. 'en').</dd><dt><label class="field_opt" for="license_id">License</label></dt><dd>
@@ -115,7 +109,7 @@
                     </py:for></select></dd>
-            <dd class="instructions basic">The licence under which the dataset is released.</dd>
+            <dd class="instructions basic">Choose from the dropdown the appropriate licence under which your data is being published. This will  already contain the licence specified in your publisher record, For more information on IATI's licensing guidelines go to <a href="http://iatistandard.org/standard/licencing">http://iatistandard.org/standard/licencing</a>.</dd><dt><label class="field_opt" for="tags">Tags</label></dt><dd>
@@ -164,28 +158,29 @@
         </table><div class="instructions basic">The files containing the data or address of the APIs for accessing it.</div>
-        <div class="instructions further"><br /><b>URL:</b> This is the Internet link directly to the data - by selecting this link in a web browser, the user will immediately download the full dataset. Note that datasets are not hosted on this site, but by the publisher of the data.<br /><b>Format:</b> This should give the file format in which the data is supplied. (i.e. IATI-XML)<br /><b>Description</b> Any information you want to add to describe the resource.<br /></div>
+        <div class="instructions further"><br /><b>URL:</b><span class="field-xml">[source-url]</span> This is the Internet link directly to the data - by selecting this link in a web browser, the user will immediately download the full dataset. Note that datasets are not hosted on this site, but by the publisher of the data.<br /><b>Format:</b><span class="field-xml">[format]</span>This should give the file format in which the data is supplied. IATI-compliant data should specify "IATI-XML".<br /><b>Description</b> Any information you want to add to describe the resource.<br /></div><div class="field_error" py:if="errors.get('resources', '')">Dataset resource(s) incomplete.</div></fieldset><fieldset><legend>Verification and Analysis </legend><dl>
-            <dt><label class="field_opt" for="activity_period">Activitiy Period</label></dt>
+            <dt><label class="field_opt" for="activity_period">Activitiy Period</label><div class="field-xml">[activiy-period-start -<br/>activity-period-end]</div></dt><dd><input class="short" id="activity_period-from" name="activity_period-from" type="text" value="${data.get('activity_period-from', '')}" /> -
                 <input class="short" id="activity_period-to" name="activity_period-to" type="text" value="${data.get('activity_period-to', '')}" /></dd>
-            <dd class="instructions basic">Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
+            <dd class="instructions basic">The earliest and latest dates reported for the starting and ending of activities within this dataset. Acceptable formats: 'DD/MM/YYYY HH:MM', 'DD/MM/YYYY', 'MM/YYYY', 'YYYY'.</dd>
 
-            <dt><label class="field_opt" for="activity_count">Num. Activities</label></dt>
+            <dt><label class="field_opt" for="activity_count">Num. Activities</label><div class="field-xml">[activity-count]</div></dt><dd><input id="activity_count" name="activity_count" size="40" type="text" value="${data.get('activity_count', '')}"/></dd>
+            <dd class="instructions basic">A count of the number of activities reported.</dd><dt><label class="field_opt" for="archive_file">Archive</label></dt><dd><input id="archive_file" name="archive_file" size="40" type="checkbox" py:attrs="{'checked': 'checked' if data.get('archive_file','') == 'yes' else None}" /></dd><py:choose><py:when test="c.is_sysadmin">
-                <dt><label class="field_opt" for="verified">Verification</label></dt>
+                <dt><label class="field_opt" for="verified">Verification</label><div class="field-xml">[verification-status]</div></dt><dd><input id="verified" name="verified" size="40" type="checkbox" py:attrs="{'checked': 'checked' if data.get('verified','') == 'yes' else None}" /></dd><dt><label class="field_opt" for="state">State</label></dt>
@@ -198,7 +193,7 @@
                 </dd></py:when></py:choose>
-
+            <dd class="instructions basic">Choose from the dropdown: "active" - published; "pending" - pre-publication; "deleted"</dd></dl></fieldset>
 



https://bitbucket.org/okfn/ckanextiati/changeset/e7f9ea1bdf30/
changeset:   e7f9ea1bdf30
user:        amercader
date:        2011-11-07 19:55:52
summary:     [merge] from spreadsheet-support
affected #:  15 files

diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c README.txt
--- a/README.txt
+++ b/README.txt
@@ -23,13 +23,10 @@
 1) Install the extension from its source repository (a Debian package 
 is not available at this time):
 
-(env)$ pip install -e hg+https://okfn@bitbucket.org/okfn/ckanext-solr#egg=ckanext-solr
 (env)$ pip install -e hg+https://okfn@bitbucket.org/okfn/ckanextiati#egg=ckanextiati
 
-(Solr is a requirement but also available in packaged form).
-
-You probably also want to install the ckanext-solr and
-ckanext-wordpresser packages.  See their respective documentation for
+You probably also want to install the ckanext-wordpresser and
+ckanext-archiver packages.  See their respective documentation for
 install notes.
 
 2) Copy or symlink the modified Solr schema.xml into the Solr core 
@@ -63,6 +60,10 @@
 
 (Don't forget to also add a 'solr_url').
 
+# User credentials used in the archiver
+iati.admin_user.name = <user_name>
+iati.admin_user.api_key = <api_key>
+ 
 
 Overall workflow for IATI
 =========================
@@ -123,3 +124,12 @@
 report data itself. 
 
 
+Archiver command
+================
+
+The extension includes a paster command that will download all IATI XML files
+(i.e. all resources), parse them and extract a couple of variables, which will
+be stored in extras. To run it you must install ckanext-archiver. To run the
+command, assuming you are on the ckanextiati directory::
+
+    paster iati-archiver update --config=../ckan/development.ini


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/authz.py
--- a/ckanext/iati/authz.py
+++ b/ckanext/iati/authz.py
@@ -7,6 +7,20 @@
 log = logging.getLogger(__name__)
 
 
+def get_user_administered_groups(user_name):
+    user = model.User.get(user_name)
+    if not user:
+        raise ValueError(user)
+
+    q = model.Session.query(model.GroupRole).filter_by(user=user,role=model.Role.ADMIN)
+
+    groups = []
+    for group_role in q.all():
+        if group_role.group.state == 'active':
+            groups.append(group_role.group.id)
+
+    return groups
+
 def _get_group_authz_group(group):
     """ For each group, we're adding an authorization group with the same settings 
         that can then be set as the owner for new packages. """


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/commands.py
--- /dev/null
+++ b/ckanext/iati/commands.py
@@ -0,0 +1,199 @@
+import os
+import datetime
+from lxml import etree
+import requests
+import json
+from pylons import config
+from ckan.lib.cli import CkanCommand
+from ckan.logic import get_action
+from ckan import model
+
+from ckan.lib.helpers import date_str_to_datetime
+from ckanext.archiver import tasks
+import logging
+
+log = logging.getLogger('iati_archiver')
+
+class Archiver(CkanCommand):
+    '''
+    Download and save copies of all IATI activity files, extract some metrics
+    from them and store them as extras.
+
+    Usage:
+
+        paster iati-archiver update [{package-id}]
+           - Archive all activity files or just those belonging to a specific package
+             if a package id is provided
+
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+    min_args = 0
+    max_args = 2
+    pkg_names = []
+
+    def command(self):
+        '''
+        Parse command line arguments and call appropriate method.
+        '''
+        if not self.args or self.args[0] in ['--help', '-h', 'help']:
+            print Archiver.__doc__
+            return
+
+        t1 = datetime.datetime.now()
+
+        cmd = self.args[0]
+        self._load_config()
+        # TODO: use this when it gets to default ckan
+        # username = get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+        context = {
+            'model': model,
+            'session':model.Session,
+            'site_url':config.get('ckan.site_url'),
+            'user': config.get('iati.admin_user.name'),
+            'apikey': config.get('iati.admin_user.api_key')
+        }
+
+        if cmd == 'update':
+            if len(self.args) > 1:
+                packages = [unicode(self.args[1])]
+
+            else:
+                packages = get_action('package_list')(context, {})
+
+            data_formats = tasks.DATA_FORMATS
+            data_formats.append('iati-xml')
+
+            log.info('IATI Archiver: starting  %s' % str(t1))
+            log.info('Number of datasets to archive: %d' % len(packages))
+            updated = 0
+            for package_id in packages:
+                package = get_action('package_show_rest')(context,{'id': package_id})
+
+                is_activity_package = (package['extras']['filetype'] == 'activity') if 'filetype' in package['extras'] else 'activity'
+
+                log.debug('Archiving dataset: %s (%d resources)' % (package.get('name'), len(package.get('resources', []))))
+                for resource in package.get('resources', []):
+
+                    if not resource.get('url',''):
+                        log.error('Resource for dataset %s does not have URL' % package['name'])
+                        continue
+
+                    try:
+                        result = tasks.download(context,resource,data_formats=data_formats)
+                    except tasks.LinkCheckerError,e:
+                        if 'method not allowed' in str(e).lower():
+                            # The DFID server does not support HEAD requests*,
+                            # so we need to handle the download manually
+                            # * But only the first time a file is downloaded!?
+                            result = _download_resource(context,resource,data_formats=data_formats)
+                        else:
+                            log.error('Invalid resource URL for dataset %s: %s' % (package['name'],str(e)))
+                            continue
+                    except tasks.DownloadError,e:
+                        log.error('Error downloading resource for dataset %s: %s' % (package['name'],str(e)))
+                        continue
+
+                    if 'zip' in result['headers']['content-type']:
+                        # Skip zipped files for now
+                        log.info('Skipping zipped file for dataset %s ' % package.get('name'))
+                        continue
+
+                    file_path = result['saved_file']
+                    f = open(file_path,'r')
+                    xml = f.read()
+                    f.close()
+                    os.remove(file_path)
+                    try:
+                        tree = etree.fromstring(xml)
+                    except etree.XMLSyntaxError,e:
+                        log.error('Could not parse XML file for dataset %s: %s' % (package['name'],str(e)))
+                        continue
+
+                    new_extras = {}
+                    if is_activity_package:
+                        # Number of activities (activity_count extra)
+                        new_extras['activity_count'] = int(tree.xpath('count(//iati-activity)'))
+
+                    # Last updated date (data_updated extra)
+                    if is_activity_package:
+                        xpath = 'iati-activity/@last-updated-datetime'
+                    else:
+                        xpath = 'iati-organisation/@last-updated-datetime'
+                    dates = tree.xpath(xpath) or []
+
+                    sorted(dates,reverse=True)
+                    last_updated_date = dates[0] if len(dates) else None
+
+                    # Check dates
+                    if last_updated_date:
+                        # Get rid of the microseconds
+                        if '.' in last_updated_date:
+                            last_updated_date = last_updated_date[:last_updated_date.find('.')]
+                        try:
+                            date = date_str_to_datetime(last_updated_date)
+                            format = '%Y-%m-%d %H:%M' if (date.hour and date.minute) else '%Y-%m-%d'
+                            new_extras['data_updated'] = date.strftime(format)
+                        except (ValueError,TypeError),e:
+                            log.error('Wrong date format for data_updated: %s' % str(e))
+
+
+                    update = False
+                    for key,value in new_extras.iteritems():
+                        if value and (not key in package['extras'] or value != package['extras'][key]):
+                            update = True
+                            old_value = package['extras'][key] if key in package['extras'] else '""'
+                            log.info('Updated extra %s for dataset %s: %s -> %s' % (key,package['name'],old_value,value))
+                            package['extras'][key] = value
+
+                    if update:
+                        context['id'] = package['id']
+                        updated_package = get_action('package_update_rest')(context,package)
+                        log.debug('Package %s updated with new extras' % package['name'])
+                        updated = updated + 1
+
+            t2 = datetime.datetime.now()
+
+            log.info('IATI Archiver: Done. Updated %i packages. Total time: %s' % (updated,str(t2 - t1)))
+        else:
+            log.error('Command %s not recognized' % (cmd,))
+
+def _download_resource(context,resource, max_content_length=50000000, url_timeout=30,data_formats=['xml','iati-xml']):
+
+    # get the resource and archive it
+    #logger.info("Resource identified as data file, attempting to archive")
+    res = requests.get(resource['url'], timeout = url_timeout)
+
+    headers = res.headers
+    resource_format = resource['format'].lower()
+    ct = headers.get('content-type', '').lower()
+    cl = headers.get('content-length')
+
+    resource_changed = (resource.get('mimetype') != ct) or (resource.get('size') != cl)
+    if resource_changed:
+        resource['mimetype'] = ct
+        resource['size'] = cl
+
+    length, hash, saved_file = tasks._save_resource(resource, res, max_content_length)
+
+    # check that resource did not exceed maximum size when being saved
+    # (content-length header could have been invalid/corrupted, or not accurate
+    # if resource was streamed)
+    #
+    # TODO: remove partially archived file in this case
+    if length >= max_content_length:
+        if resource_changed:
+            tasks._update_resource(context, resource)
+        # record fact that resource is too large to archive
+        raise tasks.DownloadError("Content-length after streaming reached maximum allowed value of %s" %
+            max_content_length)
+
+    # update the resource metadata in CKAN
+    resource['hash'] = hash
+    tasks._update_resource(context, resource)
+
+    return {'length': length,
+            'hash' :hash,
+            'headers': headers,
+            'saved_file': saved_file}
+


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/controllers/package_iati.py
--- a/ckanext/iati/controllers/package_iati.py
+++ b/ckanext/iati/controllers/package_iati.py
@@ -5,6 +5,7 @@
 from ckan.controllers.package import PackageController
 from ckan.authz import Authorizer
 
+from ckan.logic import get_action
 from ckan.logic.schema import package_form_schema
 from ckan.lib.navl.validators import (ignore_missing,
                                       not_empty,
@@ -12,11 +13,12 @@
                                       ignore,
                                       keep_extras,
                                      )
+from ckan.logic.validators import int_validator
 from ckan.logic.converters import convert_from_extras, convert_to_extras, date_to_db, date_to_form
-from ckan.lib.navl.dictization_functions import Missing, Invalid
-from ckan.lib.field_types import DateType, DateConvertError
 
 from ckanext.iati.lists import COUNTRIES
+from ckanext.iati.logic.validators import iati_dataset_name
+from ckanext.iati.logic.converters import convert_from_comma_list, convert_to_comma_list, checkbox_value
 
 class PackageIatiController(PackageController):
 
@@ -40,12 +42,14 @@
             'data_updated': [date_to_db, convert_to_extras,ignore_missing],
             'activity_period-from': [date_to_db, convert_to_extras,ignore_missing],
             'activity_period-to': [date_to_db, convert_to_extras,ignore_missing],
-            'activity_count': [integer,convert_to_extras,ignore_missing],
+            'activity_count': [int_validator,convert_to_extras,ignore_missing],
             'archive_file': [checkbox_value, convert_to_extras,ignore_missing],
             'verified': [checkbox_value, convert_to_extras,ignore_missing],
             'language': [convert_to_extras, ignore_missing],
         })
 
+        schema['name'].append(iati_dataset_name)
+
         return schema
 
     def _db_to_form_schema(self):
@@ -62,6 +66,12 @@
             'verified': [convert_from_extras,ignore_missing],
             'language': [convert_from_extras, ignore_missing],
         })
+        # Remove isodate validator
+        schema['resources'].update({
+            'last_modified': [ignore_missing],
+            'cache_last_updated': [ignore_missing],
+            'webstore_last_updated': [ignore_missing]
+        })
 
         return schema
 
@@ -86,7 +96,7 @@
             url = url.replace('<NAME>', pkgname)
         else:
             url = h.url_for(controller='package', action='read', id=pkgname)
-        redirect(url)        
+        redirect(url)
 
     # End hooks
 
@@ -103,24 +113,3 @@
         return [{'id':group.id,'name':group.name, 'title':group.title} for group in groups if group.state==model.State.ACTIVE]
 
 
-def convert_to_comma_list(value, context):
-     
-    return ', '.join(json.loads(value))
-
-def convert_from_comma_list(value, context):
-     
-    return [x.strip() for x in value.split(',') if len(x)]
-
-def checkbox_value(value,context):
-
-    return 'yes' if not isinstance(value, Missing) else 'no'
-
-def integer(value,context):
-
-    if not value == '':
-        try:
-            value = int(value)
-        except ValueError,e:
-            raise Invalid(str(e))
-        return value
-


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/controllers/spreadsheet.py
--- /dev/null
+++ b/ckanext/iati/controllers/spreadsheet.py
@@ -0,0 +1,292 @@
+import logging
+import csv
+import StringIO
+
+from ckan import model
+from ckan.lib.base import c, request, response, config, h, redirect, render, abort,  BaseController
+from ckan.lib.helpers import json
+from ckan.authz import Authorizer
+from ckan.logic import get_action, NotFound, ValidationError, NotAuthorized
+from ckan.logic.converters import date_to_db
+from ckan.logic.validators import int_validator
+from ckan.lib.navl.validators import not_empty, ignore_empty, not_missing
+from ckan.lib.navl.dictization_functions import validate
+from ckanext.iati.authz import get_user_administered_groups
+
+from ckanext.iati.logic.validators import (iati_dataset_name_from_csv, 
+                                           file_type_validator,
+                                           db_date,
+                                           yes_no,
+                                           country_code)
+
+log = logging.getLogger(__name__)
+
+CSV_MAPPING = [
+        ('registry-publisher-id', 'groups', 'name', [not_empty]),
+        ('registry-file-id', 'package', 'name', [not_empty, iati_dataset_name_from_csv]),
+        ('title', 'package', 'title', []),
+        ('contact-email', 'package', 'author_email', []),
+        ('source-url', 'resources', 'url', []),
+        ('format', 'resources', 'format', []),
+        ('file-type','extras', 'filetype', [ignore_empty, file_type_validator]),
+        ('recipient-country','extras', 'country', [ignore_empty, country_code]),
+        ('activity-period-start','extras', 'activity_period-from', [ignore_empty, db_date]),
+        ('activity-period-end','extras', 'activity_period-to', [ignore_empty, db_date]),
+        ('last-updated-datetime','extras', 'data_updated', [ignore_empty, db_date]),
+        ('generated-datetime','extras', 'record_updated', [ignore_empty, db_date]),
+        ('activity-count','extras', 'activity_count', [ignore_empty,int_validator]),
+        ('verification-status','extras', 'verified', [ignore_empty,yes_no]),
+        ('default-language','extras', 'language', [])
+        ]
+
+class CSVController(BaseController):
+
+
+    def __before__(self, action, **params):
+        super(CSVController,self).__before__(action, **params)
+
+        if not c.user:
+            abort(403,'Permission denied')
+
+        self.is_sysadmin = Authorizer().is_sysadmin(c.user)
+
+        # Groups of which the logged user is admin
+        self.authz_groups = get_user_administered_groups(c.user)
+
+    def download(self,publisher=None):
+
+        context = {'model':model,'user': c.user or c.author}
+
+        if publisher and publisher not in ['all','template']:
+            try:
+                group = get_action('group_show')(context, {'id':publisher})
+            except NotFound:
+                abort(404, 'Publisher not found')
+
+            if not group['id'] in self.authz_groups and not self.is_sysadmin:
+                abort(403,'Permission denied for this publisher group')
+
+        if self.is_sysadmin:
+            if publisher:
+                # Return CSV for provided publisher
+                output = self.write_csv_file(publisher)
+            else:
+                # Show list of all available publishers
+                c.groups = get_action('group_list')(context, {'all_fields':True})
+                return render('csv/index.html')
+        else:
+            if publisher and publisher != 'all':
+                # Return CSV for provided publisher (we already checked the permissions)
+                output = self.write_csv_file(publisher)
+            elif len(self.authz_groups) == 1:
+                # Return directly CSV for publisher
+                output = self.write_csv_file(self.authz_groups[0])
+            elif len(self.authz_groups) > 1:
+                # Show list of available publishers for this user
+                groups = get_action('group_list')(context, {'all_fields':True})
+                c.groups = []
+                for group in groups:
+                    if group['id'] in self.authz_groups:
+                        c.groups.append(group)
+
+                return render('csv/index.html')
+            else:
+                # User does not have permissions on any publisher
+                abort(403,'Permission denied')
+
+
+        file_name = publisher if publisher else self.authz_groups[0]
+        response.headers['Content-type'] = 'text/csv'
+        response.headers['Content-disposition'] = 'attachment;filename=%s.csv' % str(file_name)
+        return output
+
+    def upload(self):
+        if not self.is_sysadmin and not self.authz_groups:
+            # User does not have permissions on any publisher
+            abort(403,'Permission denied')
+
+        if request.method == 'GET':
+            return render('csv/upload.html')
+        elif request.method == 'POST':
+            csv_file = request.POST['file']
+
+            if not hasattr(csv_file,'filename'):
+                abort(400,'No CSV file provided')
+
+            c.file_name = csv_file.filename
+
+            added, updated, errors = self.read_csv_file(csv_file)
+            c.added = added
+            c.updated = updated
+
+            c.errors = errors
+
+            log.info('CSV import finished: file %s, %i added, %i updated, %i errors' % \
+                    (c.file_name,len(c.added),len(c.updated),len(c.errors)))
+
+            return render('csv/result.html')
+
+    def write_csv_file(self,publisher):
+        context = {'model':model,'user': c.user or c.author}
+        try:
+            if publisher == 'all':
+                packages = get_action('package_list')(context, {})
+            elif publisher == 'template':
+                # Just return an empty CSV file with just the headers
+                packages = []
+            else:
+                group = get_action('group_show')(context, {'id':publisher})
+                packages = [pkg['id'] for pkg in group['packages']]
+        except NotFound:
+            abort(404, 'Group not found')
+
+        f = StringIO.StringIO()
+
+        output = ''
+        try:
+            fieldnames = [n[0] for n in CSV_MAPPING]
+            writer = csv.DictWriter(f, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
+            headers = dict( (n[0],n[0]) for n in CSV_MAPPING )
+            writer.writerow(headers)
+
+            packages.sort()
+            for pkg in packages:
+                try:
+                    package = get_action('package_show_rest')(context,{'id':pkg})
+                except NotAuthorized:
+                    log.warn('User %s not authorized to read package %s' % (c.user, pkg))
+                    continue
+                if package:
+                    row = {}
+                    for fieldname, entity, key, v in CSV_MAPPING:
+                        value = None
+                        if entity == 'groups':
+                            if len(package['groups']):
+                                value = package['groups'][0]
+                        elif entity == 'resources':
+                            if len(package['resources']) and key in package['resources'][0]:
+                                value = package['resources'][0][key]
+                        elif entity == 'extras':
+                            if key in package['extras']:
+                                value = package['extras'][key]
+                        else:
+                            if key in package:
+                                value = package[key]
+                        row[fieldname] = value
+                    writer.writerow(row)
+            output = f.getvalue()
+        finally:
+            f.close()
+
+        return output
+
+    def read_csv_file(self,csv_file):
+        fieldnames = [f[0] for f in CSV_MAPPING]
+
+        try:
+            # Try to sniff the file dialect
+            dialect = csv.Sniffer().sniff(csv_file.file.read(1024))
+            csv_file.file.seek(0)
+
+            reader = csv.DictReader(csv_file.file, dialect=dialect)
+        except csv.Error,e:
+            abort(400,'Error reading CSV file: %s' % str(e))
+
+
+        log.debug('Starting reading file %s (delimiter "%s", escapechar "%s")' %
+                    (csv_file.filename,dialect.delimiter,dialect.escapechar))
+
+        # Check if all columns are present
+        if not sorted(reader.fieldnames) == sorted(fieldnames):
+            error = {'file': 'Missing columns: %s' % ' ,'.join([f for f in fieldnames if f not in reader.fieldnames])}
+            return [], [], [('1',error)]
+
+        context = {'model':model,'user': c.user or c.author, 'api_verion':'1'}
+        groups= get_action('group_list')(context, {})
+
+        counts = {'added': [], 'updated': []}
+        errors = {}
+        for i,row in enumerate(reader):
+            row_index = str(i + 1)
+            errors[row_index] = {}
+            try:
+                # We will now run the IATI specific validation, CKAN core will
+                # run the default one later on
+                schema = dict([(f[0],f[3]) for f in CSV_MAPPING])
+                row, row_errors = validate(row,schema)
+                if row_errors:
+                    for key, msgs in row_errors.iteritems():
+                        log.error('Error in row %i: %s: %s' % (i+1,key,str(msgs)))
+                        errors[row_index][key] = msgs
+                    continue
+
+                package_dict = self.get_package_dict_from_row(row)
+                self.create_or_update_package(package_dict,counts)
+
+                del errors[row_index]
+            except ValidationError,e:
+                iati_keys = dict([(f[2],f[0]) for f in CSV_MAPPING])
+                for key, msgs in e.error_dict.iteritems():
+                    iati_key = iati_keys[key]
+                    log.error('Error in row %i: %s: %s' % (i+1,iati_key,str(msgs)))
+                    errors[row_index][iati_key] = msgs
+            except NotAuthorized,e:
+                msg = 'Not authorized to publish to this group: %s' % row['registry-publisher-id']
+                log.error('Error in row %i: %s' % (i+1,msg))
+                errors[row_index]['registry-publisher-id'] = [msg]
+
+        errors = sorted(errors.iteritems())
+        return counts['added'], counts['updated'], errors
+
+    def get_package_dict_from_row(self,row):
+        package = {}
+        for fieldname, entity, key, v in CSV_MAPPING:
+            if fieldname in row:
+                # If value is None (empty cell), property will be set to blank
+                value = row[fieldname]
+                if entity == 'groups':
+                    package['groups'] = [value]
+                elif entity == 'resources':
+                    if not 'resources' in package:
+                       package['resources'] = [{}]
+                    package['resources'][0][key] = value
+                elif entity == 'extras':
+                    if not 'extras' in package:
+                       package['extras'] = {}
+                    package['extras'][key] = value
+                else:
+                    package[key] = value
+        return package
+
+    def create_or_update_package(self, package_dict, counts = None):
+
+        context = {
+            'model': model,
+            'session': model.Session,
+            'user': c.user,
+            'api_version':'1'
+        }
+
+        # Check if package exists
+        data_dict = {}
+        data_dict['id'] = package_dict['name']
+        try:
+            existing_package_dict = get_action('package_show')(context, data_dict)
+
+            # Update package
+            log.info('Package with name "%s" exists and will be updated' % package_dict['name'])
+
+            context.update({'id':existing_package_dict['id']})
+            package_dict.update({'id':existing_package_dict['id']})
+            updated_package = get_action('package_update_rest')(context, package_dict)
+            if counts:
+                counts['updated'].append(updated_package['name'])
+            log.debug('Package with name "%s" updated' % package_dict['name'])
+        except NotFound:
+            # Package needs to be created
+            log.info('Package with name "%s" does not exist and will be created' % package_dict['name'])
+            new_package = get_action('package_create_rest')(context, package_dict)
+            if counts:
+                counts['added'].append(new_package['name'])
+            log.debug('Package with name "%s" created' % package_dict['name'])
+


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/logic/converters.py
--- /dev/null
+++ b/ckanext/iati/logic/converters.py
@@ -0,0 +1,14 @@
+from ckan.lib.navl.dictization_functions import Missing
+
+def convert_to_comma_list(value, context):
+
+    return ', '.join(json.loads(value))
+
+def convert_from_comma_list(value, context):
+
+    return [x.strip() for x in value.split(',') if len(x)]
+
+def checkbox_value(value,context):
+
+    return 'yes' if not isinstance(value, Missing) else 'no'
+


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/logic/validators.py
--- /dev/null
+++ b/ckanext/iati/logic/validators.py
@@ -0,0 +1,77 @@
+from ckan.logic import get_action
+from ckan.lib.navl.dictization_functions import unflatten, Invalid
+from ckan.lib.field_types import DateType, DateConvertError
+
+from ckanext.iati.lists import FILE_TYPES, COUNTRIES
+
+def iati_dataset_name(key,data,errors,context):
+
+    unflattened = unflatten(data)
+    value = data[key]
+    for grp in unflattened['groups']:
+        if grp['id']:
+            group_id = grp['id']
+            break
+    group = get_action('group_show')(context,{'id':group_id})
+    group_name = group['name']
+
+    parts = value.split('-')
+    code_part = parts[-1]
+    group_part = parts[0] if len(parts) == 2 else '-'.join(parts[:-1])
+    if not code_part or not group_part or not group_part == group_name:
+        errors[key].append('Dataset name does not follow the convention <publisher>-<code>: "%s" (using publisher %s)' % (value,group_name))
+
+def iati_dataset_name_from_csv(key,data,errors,context):
+
+    unflattened = unflatten(data)
+    value = data[key]
+
+    if not 'registry-publisher-id' in unflattened:
+        errors[key].append('Publisher name missing')
+        return
+
+    group_name = unflattened['registry-publisher-id']
+
+    parts = value.split('-')
+    code_part = parts[-1]
+    group_part = parts[0] if len(parts) == 2 else '-'.join(parts[:-1])
+    if not code_part or not group_part or not group_part == group_name:
+        errors[key].append('Dataset name does not follow the convention <publisher>-<code>: "%s" (using publisher %s)' % (value,group_name))
+
+def file_type_validator(key,data,errors, context=None):
+    value = data.get(key)
+
+    allowed_values = [t[0] for t in FILE_TYPES]
+    if not value in allowed_values:
+        errors[key].append('File type must be one of [%s]' % ', '.join(allowed_values))
+
+def db_date(value, context):
+    try:
+        timedate_dict = DateType.parse_timedate(value, 'db')
+    except DateConvertError, e:
+        # Cannot parse
+        raise Invalid(str(e))
+    try:
+        value = DateType.format(timedate_dict, 'db')
+    except DateConvertError, e:
+        # Values out of range
+        raise Invalid(str(e))
+
+    return value
+
+def yes_no(value,context):
+
+    value = value.lower()
+    if not value in ['yes','no']:
+        raise Invalid('Value must be one of [yes, no]')
+
+    return value
+
+def country_code(value,context):
+
+    value = value.upper()
+    if not value in [c[0] for c in COUNTRIES]:
+        raise Invalid('Unknown country code "%s"' % value)
+
+    return value
+


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/plugin.py
--- a/ckanext/iati/plugin.py
+++ b/ckanext/iati/plugin.py
@@ -45,6 +45,20 @@
         map.connect('/group/new', controller=group_controller, action='new')
         map.connect('/group/edit/{id}', controller=group_controller, action='edit')
 
+        csv_controller = 'ckanext.iati.controllers.spreadsheet:CSVController'
+        map.connect('/csv/download', controller=csv_controller, action='download')
+        map.connect('/csv/download/{publisher}', controller=csv_controller, action='download')
+        map.connect('/csv/upload', controller=csv_controller, action='upload',
+                    conditions=dict(method=['GET']))
+        map.connect('/csv/upload', controller=csv_controller, action='upload',
+                    conditions=dict(method=['POST']))
+
+
+        # Redirects needed after updating the datasets name for some of the publishers
+        map.redirect('/dataset/wb-{code}','/dataset/worldbank-{code}',_redirect_code='301 Moved Permanently')
+        map.redirect('/dataset/minbuza_activities','/dataset/minbuza_nl-activities',_redirect_code='301 Moved Permanently')
+        map.redirect('/dataset/minbuza_organisation','/dataset/minbuza_nl-organisation',_redirect_code='301 Moved Permanently')
+
         return map
 
     def after_map(self, map):


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/public/css/overrides.css
--- a/ckanext/iati/public/css/overrides.css
+++ b/ckanext/iati/public/css/overrides.css
@@ -580,11 +580,12 @@
 
 /*-------- Wordpress Specific --------*/
 
-.type-page 
+.type-page ul {
+    list-style-type: none
+}
 
-
-ul {
-    list-style-type: none
+.listdiv ul {
+    list-style-type: circle
 }
 
 .entry-content ol {


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/templates/csv/index.html
--- /dev/null
+++ b/ckanext/iati/templates/csv/index.html
@@ -0,0 +1,29 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+
+  <py:def function="page_title">CSV Export</py:def>
+  <div py:match="content">
+
+    <h2 class="head">CSV Export</h2>
+
+    <div>Select a Publisher to download all its records in CSV format.</div>
+    <div><strong>Warning:</strong>For publishers with a large number of datasets it may take a while to generate the CSV file. Please be patient.</div>
+    <div>Alternatively, you can download an empty CSV <a href="/csv/download/template">Template</a>.</div>
+    <hr class="cleared" />
+    <table class="groups">
+        <tr><th>Title</th><th>Number of datasets</th><th></th></tr>
+        <py:for each="group in c.groups">
+            <tr>
+                <td>${group['display_name']}</td>
+                <td>${group['packages']}</td>
+                <td><a href="${h.url_for(controller='ckanext.iati.controllers.spreadsheet:CSVController', action='download', publisher=group['name'])}">Download</a></td>
+            </tr>
+        </py:for>
+    </table>
+  </div>
+
+
+  <xi:include href="../layout.html" />
+</html>


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/templates/csv/result.html
--- /dev/null
+++ b/ckanext/iati/templates/csv/result.html
@@ -0,0 +1,59 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+    <py:def function="page_title">CSV Import Results</py:def>
+    <div py:match="content">
+
+        <h2 class="head">CSV Import Results</h2>
+
+        <div>CSV file <strong>${c.file_name}</strong> imported. (<a href="/csv/upload">Upload another file</a>)</div>
+        <hr class="cleared" />
+        <h3>Summary</h3>
+        <ul>
+            <li><a href="#added">Datasets created: ${len(c.added)}</a></li>
+            <li><a href="#updated">Datasets updated: ${len(c.updated)}</a></li>
+            <li><a href="#errors">Errors found: ${len(c.errors)}</a></li>
+        </ul>
+        <h3><a name="added">Datasets added</a></h3>
+        <ul>
+        <py:for each="pkg in c.added">
+            <li><a href="${h.url_for(controller='package', action='read', id=pkg)}">${g.site_url}${h.url_for(controller='package', action='read', id=pkg)}</a></li>
+        </py:for>
+        <py:if test="not c.added">
+            <li><i>None</i></li>
+        </py:if>
+        </ul>
+
+        <h3><a name="updated">Datasets updated</a></h3>
+        <ul>
+        <py:for each="pkg in c.updated">
+            <li><a href="${h.url_for(controller='package', action='read', id=pkg)}">${g.site_url}${h.url_for(controller='package', action='read', id=pkg)}</a></li>
+        </py:for>
+        <py:if test="not c.updated">
+            <li><i>None</i></li>
+        </py:if>
+
+        </ul>
+
+        <h3><a name="errors">Errors found</a></h3>
+        <ul>
+        <py:for each="row,fields in c.errors">
+            <li>Line ${row}:
+                <ul>
+                <py:for each="field,msgs in fields.iteritems()">
+                    <li><strong>${field}</strong>: ${msgs}</li>
+                </py:for>
+                </ul>
+            </li>
+        </py:for>
+        <py:if test="not c.errors">
+            <li><i>None</i></li>
+        </py:if>
+
+        </ul>
+
+        </div>
+
+    <xi:include href="../layout.html" />
+</html>


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/templates/csv/upload.html
--- /dev/null
+++ b/ckanext/iati/templates/csv/upload.html
@@ -0,0 +1,45 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+    <py:def function="page_title">CSV Import</py:def>
+    <div py:match="content">
+
+        <h2 class="head">CSV Import</h2>
+        <div class="listdiv">
+        <ul>
+            <li>IATI requires that you create a registry record for each file of IATI-xml data that you publish.</li>
+            <li>You may use this import utility to register or update the metadata of these records</li>
+            <li>Please provide a spreadsheet in CSV format containing IATI registry records metadata</li>
+            <li>Your file must follow the format specified in this <a href="/csv/download/template">Template</a>.</li>
+            <li>Guidance on the contents of this file can be found <a href="http://iatiregistry.org/help_csv-import">here</a>.</li>
+        </ul>
+        </div>
+        <hr class="cleared" />
+        <fieldset>
+            <legend>Upload file</legend>
+
+            <form id="csv-upload" action="/csv/upload" method="POST" enctype="multipart/form-data">
+            <dl>
+                <dt><label for="file">CSV file</label></dt>
+                <dd><input type="file" id="file" name="file"/></dd>
+            </dl>
+            <div class="submit">
+                <input id="upload" name="upload" type="submit" value="Upload" />
+            </div>
+
+            </form>
+        </fieldset>
+        <h3>Warning:</h3>
+        <div class="listdiv">
+        <ul>
+            <li>For publishers with a large number of datasets it may take a while to upload the CSV file. Please be patient.</li>
+            <li>You may only upload records that contain your authorised registry-publisher-idi.</li>
+            <li>This import will overwrite all fields in each record specified in the file. Blank cells in the file will overwrite existing data.</li>
+            <li>This import will not delete records in the registry. To delete a record follow the instructions <a href="http://iatiregistry.org/help_delete">here</a>.</li>
+        </ul>
+        </div>
+    </div>
+
+    <xi:include href="../layout.html" />
+</html>


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/templates/layout_base.html
--- a/ckanext/iati/templates/layout_base.html
+++ b/ckanext/iati/templates/layout_base.html
@@ -116,7 +116,14 @@
             <li class="page_item" py:if="h.am_authorized_with_publisher(c, actions.PACKAGE_CREATE)">
               ${h.subnav_link(c, _('Register'), controller='package', action='new', id=None)}
             </li>
-          </ul>
+            <li class="page_item" py:if="h.am_authorized_with_publisher(c, actions.PACKAGE_CREATE)">
+              ${h.subnav_link(c, _('Download current records'), controller='ckanext.iati.controllers.spreadsheet:CSVController', action='download', id=None)}
+            </li>
+            <li class="page_item" py:if="h.am_authorized_with_publisher(c, actions.PACKAGE_CREATE)">
+              ${h.subnav_link(c, _('Upload CSV file'), controller='ckanext.iati.controllers.spreadsheet:CSVController', action='upload', id=None)}
+            </li>
+
+            </ul></li><li>${h.nav_link(c, _('Groups'), controller='group', action='index', id=None)}
           <ul class="children">


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c ckanext/iati/templates/package/form_iati.html
--- a/ckanext/iati/templates/package/form_iati.html
+++ b/ckanext/iati/templates/package/form_iati.html
@@ -45,8 +45,9 @@
         <dl><py:for each="num, group in enumerate(data.get('groups', []))"><?python
-            authorized_group = [group_authz for group_authz in c.groups_authz if group_authz['id'] == group['id']]
-            authorized_group = authorized_group[0] if authorized_group else None
+            if 'id' in group:
+                authorized_group = [group_authz for group_authz in c.groups_authz if group_authz['id'] == group['id']]
+                authorized_group = authorized_group[0] if authorized_group else None
             ?><dt py:if="'id' in group">


diff -r 248e68ff7544269c3cddf33cfd2bb134dce6f272 -r e7f9ea1bdf30b026cd70c03909924c1f43528b2c setup.py
--- a/setup.py
+++ b/setup.py
@@ -35,6 +35,6 @@
       
       [paste.paster_command]
       create-iati-fixtures = ckanext.iati.fixtures:CreateIatiFixtures
-      
+      iati-archiver=ckanext.iati.commands:Archiver
       """,
       )

Repository URL: https://bitbucket.org/okfn/ckanextiati/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list