Make the public library browsing page support several features from
authorDiane Trout <diane@caltech.edu>
Fri, 6 Mar 2009 02:09:26 +0000 (02:09 +0000)
committerDiane Trout <diane@caltech.edu>
Fri, 6 Mar 2009 02:09:26 +0000 (02:09 +0000)
django admin pages.

  * Search bar
  * Pagination
  * Filters

This took grabbing the "ChangeSet" class from django.contrib.admin and
slightly modifying, in addition to the css files.

To provide the css files I had to add the ability to serve static
files from the app level. I followed the django pattern that the
internal static pages would only be served by django when debug is true.

And because it's hard to split it out, I also added a new field
'hidden' to the library table. This allows hiding libraries from
the public library page (which is useful since a number of libraries
have a gel isolate pair, which many end users find rather confusing).

14 files changed:
docs/conv_caltech_v0.1_to_htsw.py
htsworkflow/frontend/samples/admin.py
htsworkflow/frontend/samples/changelist.py [new file with mode: 0644]
htsworkflow/frontend/samples/models.py
htsworkflow/frontend/samples/views.py
htsworkflow/frontend/settings.py
htsworkflow/frontend/static/css/base.css [new file with mode: 0644]
htsworkflow/frontend/static/css/changelists.css [new file with mode: 0644]
htsworkflow/frontend/static/css/click-table.css [new file with mode: 0644]
htsworkflow/frontend/static/css/global.css [new file with mode: 0644]
htsworkflow/frontend/static/css/layout.css [new file with mode: 0644]
htsworkflow/frontend/static/img/icon_searchbox.png [new file with mode: 0644]
htsworkflow/frontend/templates/samples/library_index.html
htsworkflow/frontend/urls.py

index 2adb0364cf964bdb002d353997c83548f3ed296d..ed8094303ac2fe0588285edc3ad05b37325665cb 100644 (file)
@@ -116,6 +116,7 @@ def main(cmdline=None):
     "library_id" varchar(30) NOT NULL,
     "library_name" varchar(100) NOT NULL UNIQUE,
     "library_species_id" integer NOT NULL REFERENCES "samples_species" ("id"),
+    "hidden" bool NOT NULL,
     "cell_line_id" integer NOT NULL REFERENCES "samples_cellline" ("id"),
     "condition_id" integer NOT NULL REFERENCES "samples_condition" ("id"),
     "antibody_id" integer NULL REFERENCES "samples_antibody" ("id"),
@@ -132,27 +133,36 @@ def main(cmdline=None):
     "avg_lib_size" integer NULL,
     "notes" text NOT NULL);''')
     c.execute('''INSERT INTO samples_library 
-      (id,library_id,library_name,library_species_id, experiment_type_id,
+      (id,library_id,library_name,library_species_id, hidden, experiment_type_id,
        cell_line_id,condition_id,replicate,made_by,creation_date,
        made_for,stopping_point,amplified_from_sample_id,
        undiluted_concentration,ten_nM_dilution,successful_pM,
        avg_lib_size,notes) 
-select library_id,library_id,library_name,library_species_id, 1,
+select library_id,library_id,library_name,library_species_id, 0, 1,
        1,           1,           1,        made_by,creation_date,
        made_for,stopping_point,amplified_from_sample_id,
        undiluted_concentration,ten_nM_dilution,successful_pM,
        0,notes from fctracker_library;''');
+
+    # mark gel isolates as "hidden"
+    c.execute('''update samples_library set hidden=1  
+              where stopping_point = "1A" or stopping_point = "1Ab";''');
+
+    # get pk for RNA-seq experiment type
     c.execute('select id from samples_experimenttype where name = "RNA-seq";')
     rna_seq_id = list(c)[0]
+    # change everything marked as rnaseq to experiment_type rnaseq
     c.execute('''update samples_library set experiment_type_id=?  where library_id in (select library_id from fctracker_library where RNASeq = 1);''', rna_seq_id)
     #c.execute('''drop table fctracker_library;''') 
-    # add many to many tables
+
+    # add affiliation linking table
     c.execute('''CREATE TABLE "samples_library_affiliations" (
     "id" integer NOT NULL PRIMARY KEY,
     "library_id" integer NOT NULL REFERENCES "samples_library" ("id"),
     "affiliation_id" integer NOT NULL REFERENCES "samples_affiliation" ("id"),
     UNIQUE ("library_id", "affiliation_id"));''')
 
+    # add library to tags linking table
     c.execute('''CREATE TABLE "samples_library_tags" (
     "id" integer NOT NULL PRIMARY KEY,
     "library_id" integer NOT NULL REFERENCES "samples_library" ("id"),
index 5967e32974c04dfc6fcef3d823e6fa1b769394a6..ffdb5d844e82a0146cb4f89fa307bfd087da1c57 100644 (file)
@@ -65,9 +65,9 @@ class LibraryOptions(admin.ModelAdmin):
     fieldsets = (
       (None, {
         'fields': (
-          ('replicate','library_id','library_name'),
+          ('library_id','library_name','hidden'),
           ('library_species'),
-          ('experiment_type'),
+          ('experiment_type', 'replicate'),
           ('cell_line','condition','antibody'),)
          }),
          ('Creation Information:', {
diff --git a/htsworkflow/frontend/samples/changelist.py b/htsworkflow/frontend/samples/changelist.py
new file mode 100644 (file)
index 0000000..dccba27
--- /dev/null
@@ -0,0 +1,239 @@
+"""
+Slightly modified version of the django admin component that handles filters and searches
+"""
+from django.contrib.admin.filterspecs import FilterSpec
+from django.contrib.admin.options import IncorrectLookupParameters
+from django.core.paginator import Paginator, InvalidPage, EmptyPage
+from django.db import models
+from django.db.models.query import QuerySet
+from django.utils.encoding import smart_str
+from django.utils.http import urlencode
+
+import operator
+
+MAX_SHOW_ALL_ALLOWED = 20000
+
+#change list settings
+ALL_VAR = 'all'
+ORDER_VAR = 'o'
+ORDER_TYPE_VAR = 'ot'
+PAGE_VAR = 'p'
+SEARCH_VAR = 'q'
+IS_POPUP_VAR = 'pop'
+ERROR_FLAG = 'e'
+
+class ChangeList(object):
+    def __init__(self, request, model, list_filter, search_fields, list_per_page, queryset=None):
+        self.model = model
+        self.opts = model._meta
+        self.lookup_opts = self.opts
+        if queryset is None:
+            self.root_query_set = model.objects
+        else:
+            self.root_query_set = queryset
+        self.list_display =  []
+        self.list_display_links = None
+        self.list_filter = list_filter
+
+        self.search_fields = search_fields
+        self.list_select_related = None
+        self.list_per_page = list_per_page
+        self.model_admin = None
+
+        try:
+            self.page_num = int(request.GET.get(PAGE_VAR,'0'))
+        except ValueError:
+            self.page_num = 0
+        self.show_all = 'all' in request.GET
+        self.params = dict(request.GET.items())
+        if PAGE_VAR in self.params:
+            del self.params[PAGE_VAR]
+        if ERROR_FLAG in self.params:
+            del self.params[ERROR_FLAG]
+
+        self.multi_page = True
+        self.can_show_all = False
+      
+        self.order_field, self.order_type = self.get_ordering()
+        self.query = request.GET.get(SEARCH_VAR, '')
+        self.query_set = self.get_query_set()
+        self.get_results(request)
+        self.filter_specs, self.has_filters = self.get_filters(request)
+
+        #self.result_count = 'result count'
+        #self.full_result_count = 'full result count'
+    def get_filters(self, request):
+        filter_specs = []
+        if self.list_filter:
+            filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter]
+            for f in filter_fields:
+                spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin)
+                if spec and spec.has_output():
+                    filter_specs.append(spec)
+        return filter_specs, bool(filter_specs)
+
+    def get_query_string(self, new_params=None, remove=None):
+        if new_params is None: new_params = {}
+        if remove is None: remove = []
+        p = self.params.copy()
+        for r in remove:
+            for k in p.keys():
+                if k.startswith(r):
+                    del p[k]
+        for k, v in new_params.items():
+            if v is None:
+                if k in p:
+                    del p[k]
+            else:
+                p[k] = v
+        return '?%s' % urlencode(p)
+
+    def get_results(self, request):
+        paginator = Paginator(self.query_set, self.list_per_page)
+        # Get the number of objects, with admin filters applied.
+        result_count = paginator.count
+
+        # Get the total number of objects, with no admin filters applied.
+        # Perform a slight optimization: Check to see whether any filters were
+        # given. If not, use paginator.hits to calculate the number of objects,
+        # because we've already done paginator.hits and the value is cached.
+        if not self.query_set.query.where:
+            full_result_count = result_count
+        else:
+            full_result_count = self.root_query_set.count()
+
+        can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
+        multi_page = result_count > self.list_per_page
+
+        # Get the list of objects to display on this page.
+        if (self.show_all and can_show_all) or not multi_page:
+            result_list = list(self.query_set)
+        else:
+            try:
+                result_list = paginator.page(self.page_num+1).object_list
+            except InvalidPage:
+                result_list = ()
+
+        self.result_count = result_count
+        self.full_result_count = full_result_count
+        self.result_list = result_list
+        self.can_show_all = can_show_all
+        self.multi_page = multi_page
+        self.paginator = paginator
+
+    def get_ordering(self):
+        lookup_opts, params = self.lookup_opts, self.params
+        # For ordering, first check the "ordering" parameter in the admin
+        # options, then check the object's default ordering. If neither of
+        # those exist, order descending by ID by default. Finally, look for
+        # manually-specified ordering from the query string.
+        ordering = lookup_opts.ordering or ['-' + lookup_opts.pk.name] 
+
+        if ordering[0].startswith('-'):
+            order_field, order_type = ordering[0][1:], 'desc'
+        else:
+            order_field, order_type = ordering[0], 'asc'
+        if ORDER_VAR in params:
+            try:
+                field_name = self.list_display[int(params[ORDER_VAR])]
+                try:
+                    f = lookup_opts.get_field(field_name)
+                except models.FieldDoesNotExist:
+                    # See whether field_name is a name of a non-field
+                    # that allows sorting.
+                    try:
+                        if callable(field_name):
+                            attr = field_name
+                        elif hasattr(self.model_admin, field_name):
+                            attr = getattr(self.model_admin, field_name)
+                        else:
+                            attr = getattr(self.model, field_name)
+                        order_field = attr.admin_order_field
+                    except AttributeError:
+                        pass
+                else:
+                    order_field = f.name
+            except (IndexError, ValueError):
+                pass # Invalid ordering specified. Just use the default.
+        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
+            order_type = params[ORDER_TYPE_VAR]
+        return order_field, order_type
+
+    def get_query_set(self):
+        qs = self.root_query_set
+        lookup_params = self.params.copy() # a dictionary of the query string
+        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
+            if i in lookup_params:
+                del lookup_params[i]
+        for key, value in lookup_params.items():
+            if not isinstance(key, str):
+                # 'key' will be used as a keyword argument later, so Python
+                # requires it to be a string.
+                del lookup_params[key]
+                lookup_params[smart_str(key)] = value
+
+            # if key ends with __in, split parameter into separate values
+            if key.endswith('__in'):
+                lookup_params[key] = value.split(',')
+
+        # Apply lookup parameters from the query string.
+        try:
+            qs = qs.filter(**lookup_params)
+        # Naked except! Because we don't have any other way of validating "params".
+        # They might be invalid if the keyword arguments are incorrect, or if the
+        # values are not in the correct type, so we might get FieldError, ValueError,
+        # ValicationError, or ? from a custom field that raises yet something else
+        # when handed impossible data.
+        except Exception, e:
+            print e
+            raise IncorrectLookupParameters
+
+        # Use select_related() if one of the list_display options is a field
+        # with a relationship.
+        if self.list_select_related:
+            qs = qs.select_related()
+        else:
+            for field_name in self.list_display:
+                try:
+                    f = self.lookup_opts.get_field(field_name)
+                except models.FieldDoesNotExist:
+                    pass
+                else:
+                    if isinstance(f.rel, models.ManyToOneRel):
+                        qs = qs.select_related()
+                        break
+
+        # Set ordering.
+        if self.order_field:
+            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
+
+        # Apply keyword searches.
+        def construct_search(field_name):
+            if field_name.startswith('^'):
+                return "%s__istartswith" % field_name[1:]
+            elif field_name.startswith('='):
+                return "%s__iexact" % field_name[1:]
+            elif field_name.startswith('@'):
+                return "%s__search" % field_name[1:]
+            else:
+                return "%s__icontains" % field_name
+
+        if self.search_fields and self.query:
+            for bit in self.query.split():
+                or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.search_fields]
+                other_qs = QuerySet(self.model)
+                other_qs.dup_select_related(qs)
+                other_qs = other_qs.filter(reduce(operator.or_, or_queries))
+                qs = qs & other_qs
+            for field_name in self.search_fields:
+                if '__' in field_name:
+                    qs = qs.distinct()
+                    break
+
+        if self.opts.one_to_one_field:
+            qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to)
+
+        return qs
+
+
index a3c27abe7a793b74ae4be15b3d3d325af7767e26..95f6376a5e6ba72afa47262a67dd734ab8a74dbd 100644 (file)
@@ -118,6 +118,8 @@ class Library(models.Model):
   library_id = models.CharField(max_length=30, db_index=True)
   library_name = models.CharField(max_length=100, unique=True)
   library_species = models.ForeignKey(Species)
+  # new field 2008 Mar 5, alter table samples_library add column "hidden" NOT NULL default 0;
+  hidden = models.BooleanField()
   cell_line = models.ForeignKey(Cellline)
   condition = models.ForeignKey(Condition)
   antibody = models.ForeignKey(Antibody,blank=True,null=True)
index f36a0c3060932af86a43d8889758286f532fe2c8..a6a996cb7c42a2084b5e169a491f2df4543ea872 100644 (file)
@@ -1,4 +1,5 @@
 # Create your views here.
+from htsworkflow.frontend.samples.changelist import ChangeList
 from htsworkflow.frontend.samples.models import Library
 from htsworkflow.frontend.samples.results import get_flowcell_result_dict, parse_flowcell_id
 from htsworkflow.pipelines.runfolder import load_pipeline_run_xml
@@ -8,8 +9,8 @@ from htsworkflow.util import makebed
 from htsworkflow.util import opener
 
 from django.http import HttpResponse
+from django.template import RequestContext
 from django.template.loader import get_template
-from django.template import Context
 
 import StringIO
 import logging
@@ -17,12 +18,13 @@ import os
 
 LANE_LIST = [1,2,3,4,5,6,7,8]
 
-def create_library_list():
+def create_library_context(cl):
     """
     Create a list of libraries that includes how many lanes were run
     """
-    library_list = []
-    for lib in Library.objects.order_by('-library_id'):
+    records = []
+    #for lib in library_items.object_list:
+    for lib in cl.result_list:
        summary = {}
        summary['library_id'] = lib.library_id
        summary['library_name'] = lib.library_name
@@ -32,13 +34,23 @@ def create_library_list():
            lane = getattr(lib, 'lane_%d_library' % (lane_id,))
            lanes_run += len( lane.all() )
        summary['lanes_run'] = lanes_run
-       library_list.append(summary)
-    return library_list
+       records.append(summary)
+    cl.result_count = unicode(cl.paginator._count) + u" libraries"
+    return {'library_list': records }
 
 def library(request):
-    library_list = create_library_list()
+   # build changelist
+    fcl = ChangeList(request, Library,
+        list_filter=['library_species','affiliations'],
+        search_fields=['library_id', 'library_name'],
+        list_per_page=25,
+        queryset=Library.objects.filter(hidden__exact=0)
+    )
+
+    context = { 'cl': fcl}
+    context.update(create_library_context(fcl))
     t = get_template('samples/library_index.html')
-    c = Context({'library_list': library_list })
+    c = RequestContext(request, context)
     return HttpResponse( t.render(c) )
 
 def library_to_flowcells(request, lib_id):
@@ -100,7 +112,7 @@ def library_to_flowcells(request, lib_id):
             output.append(err)
    
     output.append('<br />')
-    output.append(t.render(Context({'lane_summary_list': lane_summary_list})))
+    output.append(t.render(RequestContext(request, {'lane_summary_list': lane_summary_list})))
     output.append('<br />')
     
     if record_count == 0:
index a988694d5d503e0073c71ef83744d9e31954819b..01ec35aa3002ef7b5c971692a4c5931bf3a3c3e1 100644 (file)
@@ -102,11 +102,11 @@ USE_I18N = True
 
 # Absolute path to the directory that holds media.
 # Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
+MEDIA_ROOT = os.path.abspath(os.path.split(__file__)[0]) + '/static/'
 
 # URL that handles the media served from MEDIA_ROOT.
 # Example: "http://media.lawrence.com"
-MEDIA_URL = ''
+MEDIA_URL = '/static/'
 
 # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
 # trailing slash.
@@ -157,12 +157,13 @@ INSTALLED_APPS = (
 
 # Project specific settings
 
-ALLOWED_IPS={'localhost': '127.0.0.1'}
+ALLOWED_IPS={'127.0.0.1': '127.0.0.1'}
 options_to_dict(ALLOWED_IPS, 'allowed_hosts')
 
-ALLOWED_ANALYS_IPS = {'localhost': '127.0.0.1'}
+ALLOWED_ANALYS_IPS = {'127.0.0.1': '127.0.0.1'}
 options_to_dict(ALLOWED_ANALYS_IPS, 'allowed_analysis_hosts')
 #UPLOADTO_HOME = os.path.abspath('../../uploads')
 #UPLOADTO_CONFIG_FILE = os.path.join(UPLOADTO_HOME, 'eland_config')
 #UPLOADTO_ELAND_RESULT_PACKS = os.path.join(UPLOADTO_HOME, 'eland_results')
 #UPLOADTO_BED_PACKS = os.path.join(UPLOADTO_HOME, 'bed_packs')
+RESULT_HOME_DIR='/Users/diane/proj/solexa/results/flowcells'
diff --git a/htsworkflow/frontend/static/css/base.css b/htsworkflow/frontend/static/css/base.css
new file mode 100644 (file)
index 0000000..9760d67
--- /dev/null
@@ -0,0 +1,14 @@
+/*
+    DJANGO Admin
+    by Wilson Miner wilson@lawrence.com
+*/
+
+/* Block IE 5 */
+@import "null.css?\"\{";
+
+/* Import other styles */
+@import url('global.css');
+@import url('layout.css');
+
+/* Import patch for IE 6 Windows */
+/*\*/ @import "patch-iewin.css"; /**/
diff --git a/htsworkflow/frontend/static/css/changelists.css b/htsworkflow/frontend/static/css/changelists.css
new file mode 100644 (file)
index 0000000..4834be4
--- /dev/null
@@ -0,0 +1,50 @@
+@import url('base.css');
+
+/* CHANGELISTS */
+#changelist { position:relative; width:100%; }
+#changelist table { width:100%; }
+.change-list .filtered table { border-right:1px solid #ddd;  }
+.change-list .filtered { min-height:400px; }
+.change-list .filtered { background:white url(../img/admin/changelist-bg.gif) top right repeat-y !important; }
+.change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { margin-right:160px !important; width:auto !important; }
+.change-list .filtered table tbody th { padding-right:1em; }
+#changelist .toplinks { border-bottom:1px solid #ccc !important; }
+#changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/admin/nav-bg.gif) 0 180% repeat-x; overflow:hidden; }
+.change-list .filtered .paginator { border-right:1px solid #ddd; }
+
+/*  CHANGELIST TABLES  */
+#changelist table thead th { white-space:nowrap; }
+#changelist table tbody td { border-left: 1px solid #ddd; }
+#changelist table tfoot { color: #666; }
+
+/*  TOOLBAR  */
+#changelist #toolbar { padding:3px; border-bottom:1px solid #ddd; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; color:#666; }
+#changelist #toolbar form input { font-size:11px; padding:1px 2px; }
+#changelist #toolbar form #searchbar { padding:2px; }
+#changelist #changelist-search img { vertical-align:middle; }
+
+/*  FILTER COLUMN  */
+#changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; }
+#changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; }
+#changelist-filter h3 { font-size:12px; margin-bottom:0; }
+#changelist-filter ul { padding-left:0;margin-left:10px; }
+#changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; }
+#changelist-filter a { color:#999; }
+#changelist-filter a:hover { color:#036; }
+#changelist-filter li.selected { border-left:5px solid #ccc; padding-left:5px;margin-left:-10px; }
+#changelist-filter li.selected a { color:#5b80b2 !important; }
+
+/*  DATE DRILLDOWN  */
+.change-list ul.toplinks { display:block; background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; border-top:1px solid white; float:left;  padding:0 !important;  margin:0 !important; width:100%; }
+.change-list ul.toplinks li { float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none; }
+.change-list ul.toplinks .date-back a { color:#999; }
+.change-list ul.toplinks .date-back a:hover { color:#036; }
+
+/* PAGINATOR */
+.paginator { font-size:11px; padding-top:10px; padding-bottom:10px; line-height:22px; margin:0; border-top:1px solid #ddd; }
+.paginator a:link, .paginator a:visited { padding:2px 6px; border:solid 1px #ccc; background:white; text-decoration:none; }
+.paginator a.showall { padding:0 !important; border:none !important; }
+.paginator a.showall:hover { color:#036 !important; background:transparent !important; }
+.paginator .end { border-width:2px !important; margin-right:6px; }
+.paginator .this-page { padding:2px 6px; font-weight:bold; font-size:13px; vertical-align:top; }
+.paginator a:hover { color:white; background:#5b80b2; border-color:#036; }
diff --git a/htsworkflow/frontend/static/css/click-table.css b/htsworkflow/frontend/static/css/click-table.css
new file mode 100644 (file)
index 0000000..1d48412
--- /dev/null
@@ -0,0 +1,19 @@
+table, td {
+  border-style: solid;
+}
+table {
+  border-width: 0 0 1px 1px;
+  border-spacing: 0;
+  border-collapse: collapse;
+}
+thead {
+  text-align: center;
+}
+td {
+  margin: 0;
+  padding: 4px;
+  border-width: 1px 1px 0 0;
+}
+td a {
+  display: block;
+}
diff --git a/htsworkflow/frontend/static/css/global.css b/htsworkflow/frontend/static/css/global.css
new file mode 100644 (file)
index 0000000..7b1a013
--- /dev/null
@@ -0,0 +1,142 @@
+body { margin:0; padding:0; font-size:12px; font-family:"Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; }
+
+/* LINKS */
+a:link, a:visited { color: #5b80b2; text-decoration:none; }
+a:hover { color: #036; }
+a img { border:none; }
+a.section:link, a.section:visited { color: white; text-decoration:none; }
+
+/* GLOBAL DEFAULTS */
+p, ol, ul, dl { margin:.2em 0 .8em 0; }
+p { padding:0; line-height:140%; }
+
+h1,h2,h3,h4,h5 { font-weight:bold; }
+h1 { font-size:18px; color:#666; padding:0 6px 0 0; margin:0 0 .2em 0; }
+h2 { font-size:16px; margin:1em 0 .5em 0; }
+h2.subhead { font-weight:normal;margin-top:0; }
+h3 { font-size:14px; margin:.8em 0 .3em 0; color:#666; font-weight:bold; }
+h4 { font-size:12px; margin:1em 0 .8em 0; padding-bottom:3px; }
+h5 { font-size:10px; margin:1.5em 0 .5em 0; color:#666; text-transform:uppercase; letter-spacing:1px; }
+
+ul li { list-style-type:square; padding:1px 0; }
+ul.plainlist { margin-left:0 !important; }
+ul.plainlist li { list-style-type:none; }
+li ul { margin-bottom:0; }
+li, dt, dd { font-size:11px; line-height:14px; }
+dt { font-weight:bold; margin-top:4px; }
+dd { margin-left:0; }
+
+form { margin:0; padding:0; }
+fieldset { margin:0; padding:0; }
+
+blockquote { font-size:11px; color:#777; margin-left:2px; padding-left:10px; border-left:5px solid #ddd; }
+code, pre { font-family:"Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; background:inherit; color:#666; font-size:11px; }
+pre.literal-block { margin:10px; background:#eee; padding:6px 8px; }
+code strong { color:#930; }
+hr { clear:both; color:#eee; background-color:#eee; height:1px; border:none; margin:0; padding:0; font-size:1px; line-height:1px; }
+
+/* TEXT STYLES & MODIFIERS */
+.small { font-size:11px; }
+.tiny { font-size:10px; }
+p.tiny { margin-top:-2px; }
+.mini { font-size:9px; }
+p.mini { margin-top:-3px; }
+.help, p.help { font-size:10px !important; color:#999; }
+p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; }
+.quiet, a.quiet:link, a.quiet:visited { color:#999 !important;font-weight:normal !important; }
+.quiet strong { font-weight:bold !important; }
+.float-right { float:right; }
+.float-left { float:left; }
+.clear { clear:both; }
+.align-left { text-align:left; }
+.align-right { text-align:right; }
+.example { margin:10px 0; padding:5px 10px; background:#efefef; }
+.nowrap { white-space:nowrap; }
+
+/* TABLES */
+table { border-collapse:collapse; border-color:#ccc; }
+td, th { font-size:11px; line-height:13px; border-bottom:1px solid #eee; vertical-align:top; padding:5px; font-family:"Lucida Grande", Verdana, Arial, sans-serif; }
+th { text-align:left; font-size:12px; font-weight:bold; }
+thead th, 
+tfoot td { color:#666; padding:2px 5px; font-size:11px; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; border-left:1px solid #ddd; border-bottom:1px solid #ddd; }
+tfoot td { border-bottom:none; border-top:1px solid #ddd; }
+thead th:first-child, 
+tfoot td:first-child { border-left:none !important; }
+thead th.optional { font-weight:normal !important; }
+fieldset table { border-right:1px solid #eee; }
+tr.row-label td { font-size:9px; padding-top:2px; padding-bottom:0; border-bottom:none; color:#666; margin-top:-1px; }
+tr.alt { background:#f6f6f6; }
+.row1 { background:#EDF3FE; }
+.row2 { background:white; }
+
+/* SORTABLE TABLES */
+thead th a:link, thead th a:visited { color:#666; display:block; }
+table thead th.sorted { background-position:bottom left !important; }
+table thead th.sorted a { padding-right:13px; }
+table thead th.ascending a { background:url(../img/admin/arrow-down.gif) right .4em no-repeat; }
+table thead th.descending a { background:url(../img/admin/arrow-up.gif) right .4em no-repeat; }
+
+/* ORDERABLE TABLES */
+table.orderable tbody tr td:hover { cursor:move; }
+table.orderable tbody tr td:first-child { padding-left:14px; background-image:url(../img/admin/nav-bg-grabber.gif); background-repeat:repeat-y; }
+table.orderable-initalized .order-cell, body>tr>td.order-cell { display:none; }
+
+/* FORM DEFAULTS */
+input, textarea, select { margin:2px 0; padding:2px 3px; vertical-align:middle; font-family:"Lucida Grande", Verdana, Arial, sans-serif; font-weight:normal; font-size:11px; }
+textarea { vertical-align:top !important; }
+input[type=text], input[type=password], textarea, select, .vTextField { border:1px solid #ccc; }
+
+/*  FORM BUTTONS  */
+.button, input[type=submit], input[type=button], .submit-row input { background:white url(../img/admin/nav-bg.gif) bottom repeat-x; padding:3px; color:black; border:1px solid #bbb; border-color:#ddd #aaa #aaa #ddd; }
+.button:active, input[type=submit]:active, input[type=button]:active { background-image:url(../img/admin/nav-bg-reverse.gif); background-position:top; }
+.button.default, input[type=submit].default, .submit-row input.default { border:2px solid #5b80b2; background:#7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x; font-weight:bold; color:white; float:right; }
+.button.default:active, input[type=submit].default:active { background-image:url(../img/admin/default-bg-reverse.gif); background-position:top; }
+
+/* MODULES */
+.module { border:1px solid #ccc; margin-bottom:5px; background:white; }
+.module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; }
+.module blockquote { margin-left:12px; }
+.module ul, .module ol { margin-left:1.5em; }
+.module h3 { margin-top:.6em; }
+.module h2, .module caption, .inline-group h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; background:#7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x; color:white; }
+.module table { border-collapse: collapse; }
+
+/* MESSAGES & ERRORS */
+ul.messagelist { padding:0 0 5px 0; margin:0; }
+ul.messagelist li { font-size:12px; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border-bottom:1px solid #ddd; color:#666; background:#ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; }
+.errornote { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:red;background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; }
+ul.errorlist { margin:0 !important; padding:0 !important; }
+.errorlist li { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:white; background:red url(../img/admin/icon_alert.gif) 5px .3em no-repeat; }
+td ul.errorlist { margin:0 !important; padding:0 !important; }
+td ul.errorlist li { margin:0 !important; }
+.errors { background:#ffc; }
+.errors input, .errors select { border:1px solid red; }
+div.system-message { background: #ffc; margin: 10px; padding: 6px 8px; font-size: .8em; }
+div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; color:red; background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; }
+.description { font-size:12px; padding:5px 0 0 12px; }
+
+/* BREADCRUMBS */
+div.breadcrumbs { background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; padding:2px 8px 3px 8px; font-size:11px;  color:#999;  border-top:1px solid white; border-bottom:1px solid #ccc; text-align:left; }
+
+/* ACTION ICONS */
+.addlink { padding-left:12px; background:url(../img/admin/icon_addlink.gif) 0 .2em no-repeat; }
+.changelink { padding-left:12px; background:url(../img/admin/icon_changelink.gif) 0 .2em no-repeat; }
+.deletelink { padding-left:12px; background:url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat; }
+a.deletelink:link, a.deletelink:visited { color:#CC3434; }
+a.deletelink:hover { color:#993333; }
+
+/* OBJECT TOOLS */
+.object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; }
+.form-row .object-tools { margin-top:5px; margin-bottom:5px; float:none; height:2em; padding-left:3.5em; }
+.object-tools li { display:block; float:left; background:url(../img/admin/tool-left.gif) 0 0 no-repeat; padding:0 0 0 8px; margin-left:2px; height:16px; }
+.object-tools li:hover { background:url(../img/admin/tool-left_over.gif) 0 0 no-repeat; }
+.object-tools a:link, .object-tools a:visited { display:block; float:left; color:white; padding:.1em 14px .1em 8px; height:14px; background:#999 url(../img/admin/tool-right.gif) 100% 0 no-repeat; }
+.object-tools a:hover, .object-tools li:hover a { background:#5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat; }
+.object-tools a.viewsitelink, .object-tools a.golink { background:#999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat; padding-right:28px; }
+.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { background:#5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat; }
+.object-tools a.addlink { background:#999 url(../img/admin/tooltag-add.gif) top right no-repeat; padding-right:28px; }
+.object-tools a.addlink:hover { background:#5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat; }
+
+/* OBJECT HISTORY */
+table#change-history { width:100%; }
+table#change-history tbody th { width:16em; }
diff --git a/htsworkflow/frontend/static/css/layout.css b/htsworkflow/frontend/static/css/layout.css
new file mode 100644 (file)
index 0000000..17c5286
--- /dev/null
@@ -0,0 +1,29 @@
+/* PAGE STRUCTURE */
+#container { position:relative; width:100%; min-width:760px; padding:0; }
+#content { margin:10px 15px; }
+#header { width:100%; }
+#content-main { float:left; width:100%; }
+#content-related { float:right; width:18em; position:relative; margin-right:-19em; }
+#footer { clear:both; padding:10px; }
+
+/*  COLUMN TYPES  */
+.colMS { margin-right:20em !important; }
+.colSM { margin-left:20em !important; }
+.colSM #content-related { float:left; margin-right:0; margin-left:-19em; }
+.colSM #content-main { float:right; }
+.popup .colM { width:95%; }
+.subcol { float:left; width:46%; margin-right:15px; }
+.dashboard #content { width:500px; }
+
+/*  HEADER  */
+#header { background:#417690; color:#ffc; overflow:hidden; }
+#header a:link, #header a:visited { color:white; }
+#header a:hover { text-decoration:underline; }
+#branding h1 { padding:0 10px; font-size:18px; margin:8px 0; font-weight:normal; color:#f4f379; }
+#branding h2 { padding:0 10px; font-size:14px; margin:-8px 0 8px 0; font-weight:normal; color:#ffc; }
+#user-tools { position:absolute; top:0; right:0; padding:1.2em 10px; font-size:11px; text-align:right; }
+
+/* SIDEBAR */
+#content-related h3 { font-size:12px; color:#666; margin-bottom:3px; }
+#content-related h4 { font-size:11px; }
+#content-related .module h2 { background:#eee url(../img/admin/nav-bg.gif) bottom left repeat-x; color:#666; }
\ No newline at end of file
diff --git a/htsworkflow/frontend/static/img/icon_searchbox.png b/htsworkflow/frontend/static/img/icon_searchbox.png
new file mode 100644 (file)
index 0000000..8ab579e
Binary files /dev/null and b/htsworkflow/frontend/static/img/icon_searchbox.png differ
index 19e30c7f1cbe56ea3038a6bc9085c01bb961fe4f..cb91b691a2951d9c42545596861a7649177468f9 100644 (file)
@@ -1,26 +1,24 @@
-<style type="text/css">
-  /* <![CDATA[ */
-  table, td {
-    border-style: solid;
-  }
-  table {
-    border-width: 0 0 1px 1px;
-    border-spacing: 0;
-    border-collapse: collapse;
-  }
-  thead {
-    text-align: center;
-  }
-  td {
-    margin: 0;
-    padding: 4px;
-    border-width: 1px 1px 0 0;
-  }
-  td a {
-    display: block;
-  }
-  /* ]]> */
-</style>
+{% load i18n %}
+{% load admin_list %}
+
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL}}css/changelists.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL}}css/click-table.css" />
+
+<div id="content" class="flex>
+  <form action="" method="get">
+    {% block search %}{% search_form cl %}{% endblock %}
+    {% block pagination %}{% pagination cl %}{% endblock %}
+  </form>
+  {% block filters %}
+  {% if cl.has_filters %}
+  <div id="changelist-filter">
+    <h2>{% trans 'Filter' %}</h2>
+    {% for spec in cl.filter_specs %}
+       {% admin_list_filter cl spec %}
+    {% endfor %}
+  </div>
+  {% endif %}
+  {% endblock %}
 
 {% block summary_stats %}
 <table>
@@ -43,4 +41,5 @@
     {% endfor %}
   </tbody>
 </table>
+</div>
 {% endblock %}
index ccc31b35be2d5f07587dc1db4fe75be62fc8ad9a..05691822f079009f36168885d43cdf72d7e1c30f 100644 (file)
@@ -8,6 +8,8 @@ admin.autodiscover()
 #databrowse.site.register(Library)
 #databrowse.site.register(FlowCell)
 
+from htsworkflow.frontend import settings
+
 urlpatterns = patterns('',
     # Base:
     (r'^eland_config/', include('htsworkflow.frontend.eland_config.urls')),
@@ -36,3 +38,9 @@ urlpatterns = patterns('',
     # databrowser
     #(r'^databrowse/(.*)', databrowse.site.root)
 )
+
+if settings.DEBUG:
+  urlpatterns += patterns('',
+      (r'^static/(?P<path>.*)$', 'django.views.static.serve', 
+        {'document_root': settings.MEDIA_ROOT}),
+  )