From 4d8e6f651a8762fa8524837c437c2f8e25ad121f Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Fri, 9 Jan 2009 10:34:42 +0100
Subject: [PATCH 01/16] =?utf8?q?Small=20changes=20to=20Dariusz=20Ga=C5=82e?=
=?utf8?q?cki=20contribution=20description=20in=20about=5Fus.html.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit
---
wolnelektury/templates/info/about_us.html | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/wolnelektury/templates/info/about_us.html b/wolnelektury/templates/info/about_us.html
index 7005583ef..f529bf700 100644
--- a/wolnelektury/templates/info/about_us.html
+++ b/wolnelektury/templates/info/about_us.html
@@ -34,7 +34,8 @@
i prof. Piotr ÅliwiÅski.
DigitalizacjÄ
i korektÄ
tekstów zajmuje siÄ Biblioteka Narodowa. Serwis internetowy zostaÅ zaprojektowany
- przez 2ia. Autorem jezyka XML jest Dariusz GaÅecki. ObsÅugÄ prawnÄ
Wolnych Lektur zapewnia Kancelaria Grynhoff,
+ przez 2ia. Autorem jÄzyka skÅadu tekstów Wolnych Lektur opartego na jÄzyku XML jest Dariusz GaÅecki. ObsÅugÄ
+ prawnÄ
Wolnych Lektur zapewnia Kancelaria Grynhoff,
Woźny, MaliÅski. Hosting serwisu zapewnia
firma EO Networks. W opracowaniu technicznym tekstów pomaga wydawnictwo Korporacja Ha!Art. Logo Wolne Lektury
jest dzieÅem agencji PZL. Projekt objÄli patronatem medialnym: Dziennik, Elle, Tok.fm, Biblioteka Analiz,
--
2.20.1
From d9d5b7bc7f432058fd3bccfb096c88438e5364a9 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Sat, 10 Jan 2009 19:31:06 +0100
Subject: [PATCH 02/16] Added MP3 and OGG to book short_html view.
---
apps/catalogue/models.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py
index 6ab090df7..0cd19cc0c 100644
--- a/apps/catalogue/models.py
+++ b/apps/catalogue/models.py
@@ -120,11 +120,15 @@ class Book(models.Model):
if self.html_file:
formats.append(u'Czytaj online' % reverse('book_text', kwargs={'slug': self.slug}))
if self.pdf_file:
- formats.append(u'Plik PDF' % self.pdf_file.url)
+ formats.append(u'PDF' % self.pdf_file.url)
if self.odt_file:
- formats.append(u'Plik ODT' % self.odt_file.url)
+ formats.append(u'ODT' % self.odt_file.url)
if self.txt_file:
- formats.append(u'Plik TXT' % self.txt_file.url)
+ formats.append(u'TXT' % self.txt_file.url)
+ if self.mp3_file:
+ formats.append(u'MP3' % self.mp3_file.url)
+ if self.ogg_file:
+ formats.append(u'OGG' % self.ogg_file.url)
self._short_html = unicode(render_to_string('catalogue/book_short.html',
{'book': self, 'tags': tags, 'formats': formats}))
--
2.20.1
From f05daac38bd815128ba11b77b1b40dc03d2b5fcf Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Sat, 21 Feb 2009 19:46:29 +0100
Subject: [PATCH 03/16] =?utf8?q?Uaktualnienie=20django-south.=20Wy=C5=9Bwi?=
=?utf8?q?etlanie=20link=C3=B3w=20do=20Lektury.Gazeta.pl=20na=20stronach?=
=?utf8?q?=20lektur=20i=20w=20katalogu.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit
---
.../migrations/0004_add_gazeta_links.py | 13 +
apps/catalogue/models.py | 2 +
apps/south/__init__.py | 4 +-
apps/south/db/generic.py | 295 +++++++++++++-----
apps/south/db/mysql.py | 41 ++-
apps/south/db/postgresql_psycopg2.py | 24 +-
apps/south/db/sql_server/__init__.py | 0
apps/south/db/sql_server/pyodbc.py | 25 ++
apps/south/db/sqlite3.py | 34 +-
apps/south/install/README | 1 -
apps/south/install/setup.py | 13 -
apps/south/management/commands/migrate.py | 17 +-
.../management/commands/startmigration.py | 194 ++++++++++--
apps/south/management/commands/syncdb.py | 31 +-
apps/south/management/commands/test.py | 12 +
apps/south/migration.py | 182 ++++++++---
apps/south/setup.py | 26 ++
apps/south/tests/db.py | 106 ++++++-
.../fakeapp/migrations/0003_alter_spam.py | 12 +
apps/south/tests/logic.py | 45 ++-
.../templates/catalogue/book_detail.html | 7 +-
.../catalogue/tagged_object_list.html | 3 +
22 files changed, 883 insertions(+), 204 deletions(-)
create mode 100644 apps/catalogue/migrations/0004_add_gazeta_links.py
create mode 100644 apps/south/db/sql_server/__init__.py
create mode 100644 apps/south/db/sql_server/pyodbc.py
delete mode 100644 apps/south/install/README
delete mode 100755 apps/south/install/setup.py
create mode 100644 apps/south/management/commands/test.py
create mode 100755 apps/south/setup.py
create mode 100644 apps/south/tests/fakeapp/migrations/0003_alter_spam.py
diff --git a/apps/catalogue/migrations/0004_add_gazeta_links.py b/apps/catalogue/migrations/0004_add_gazeta_links.py
new file mode 100644
index 000000000..d750a673f
--- /dev/null
+++ b/apps/catalogue/migrations/0004_add_gazeta_links.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from django.db import models
+
+
+class Migration:
+ def forwards(self):
+ db.add_column('catalogue_tag', 'gazeta_link', models.CharField(blank=True, max_length=240))
+ db.add_column('catalogue_book', 'gazeta_link', models.CharField(blank=True, max_length=240))
+
+ def backwards(self):
+ db.delete_column('catalogue_tag', 'gazeta_link')
+ db.delete_column('catalogue_book', 'gazeta_link')
diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py
index 0cd19cc0c..5bfe6cbb8 100644
--- a/apps/catalogue/models.py
+++ b/apps/catalogue/models.py
@@ -47,6 +47,7 @@ class Tag(TagBase):
user = models.ForeignKey(User, blank=True, null=True)
book_count = models.IntegerField(_('book count'), default=0, blank=False, null=False)
+ gazeta_link = models.CharField(blank=True, max_length=240)
def has_description(self):
return len(self.description) > 0
@@ -88,6 +89,7 @@ class Book(models.Model):
_short_html = models.TextField(_('short HTML'), editable=False)
parent_number = models.IntegerField(_('parent number'), default=0)
extra_info = JSONField(_('extra information'))
+ gazeta_link = models.CharField(blank=True, max_length=240)
# Formats
xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
diff --git a/apps/south/__init__.py b/apps/south/__init__.py
index e797d8585..3e5972eeb 100644
--- a/apps/south/__init__.py
+++ b/apps/south/__init__.py
@@ -2,5 +2,5 @@
South - Useable migrations for Django apps
"""
-__version__ = "0.3"
-__authors__ = ["Andrew Godwin ", "Andy McCurdy "]
\ No newline at end of file
+__version__ = "0.4"
+__authors__ = ["Andrew Godwin ", "Andy McCurdy "]
diff --git a/apps/south/db/generic.py b/apps/south/db/generic.py
index 09dde0362..4a5b512d7 100644
--- a/apps/south/db/generic.py
+++ b/apps/south/db/generic.py
@@ -1,10 +1,23 @@
+import datetime
from django.core.management.color import no_style
from django.db import connection, transaction, models
from django.db.backends.util import truncate_name
+from django.db.models.fields import NOT_PROVIDED
from django.dispatch import dispatcher
from django.conf import settings
+
+def alias(attrname):
+ """
+ Returns a function which calls 'attrname' - for function aliasing.
+ We can't just use foo = bar, as this breaks subclassing.
+ """
+ def func(self, *args, **kwds):
+ return getattr(self, attrname)(*args, **kwds)
+ return func
+
+
class DatabaseOperations(object):
"""
@@ -12,10 +25,14 @@ class DatabaseOperations(object):
Some of this code comes from Django Evolution.
"""
+ # We assume the generic DB can handle DDL transactions. MySQL wil change this.
+ has_ddl_transactions = True
+
def __init__(self):
self.debug = False
self.deferred_sql = []
-
+ self.dry_run = False
+ self.pending_create_signals = []
def execute(self, sql, params=[]):
"""
@@ -25,31 +42,50 @@ class DatabaseOperations(object):
cursor = connection.cursor()
if self.debug:
print " = %s" % sql, params
+
+ if self.dry_run:
+ return []
+
cursor.execute(sql, params)
try:
return cursor.fetchall()
except:
return []
-
-
+
+
def add_deferred_sql(self, sql):
"""
Add a SQL statement to the deferred list, that won't be executed until
this instance's execute_deferred_sql method is run.
"""
self.deferred_sql.append(sql)
-
-
+
+
def execute_deferred_sql(self):
"""
Executes all deferred SQL, resetting the deferred_sql list
"""
for sql in self.deferred_sql:
self.execute(sql)
-
+
self.deferred_sql = []
+ def clear_deferred_sql(self):
+ """
+ Resets the deferred_sql list to empty.
+ """
+ self.deferred_sql = []
+
+
+ def clear_run_data(self):
+ """
+ Resets variables to how they should be before a run. Used for dry runs.
+ """
+ self.clear_deferred_sql()
+ self.pending_create_signals = []
+
+
def create_table(self, table_name, fields):
"""
Creates the table 'table_name'. 'fields' is a tuple of fields,
@@ -57,14 +93,22 @@ class DatabaseOperations(object):
django.db.models.fields.Field object
"""
qn = connection.ops.quote_name
+
+ # allow fields to be a dictionary
+ # removed for now - philosophical reasons (this is almost certainly not what you want)
+ #try:
+ # fields = fields.items()
+ #except AttributeError:
+ # pass
+
columns = [
self.column_sql(table_name, field_name, field)
for field_name, field in fields
]
-
+
self.execute('CREATE TABLE %s (%s);' % (qn(table_name), ', '.join([col for col in columns if col])))
-
- add_table = create_table # Alias for consistency's sake
+
+ add_table = alias('create_table') # Alias for consistency's sake
def rename_table(self, old_table_name, table_name):
@@ -86,16 +130,26 @@ class DatabaseOperations(object):
qn = connection.ops.quote_name
params = (qn(table_name), )
self.execute('DROP TABLE %s;' % params)
-
- drop_table = delete_table
+
+ drop_table = alias('delete_table')
- def add_column(self, table_name, name, field):
+ def clear_table(self, table_name):
+ """
+ Deletes all rows from 'table_name'.
+ """
+ qn = connection.ops.quote_name
+ params = (qn(table_name), )
+ self.execute('DELETE FROM %s;' % params)
+
+ add_column_string = 'ALTER TABLE %s ADD COLUMN %s;'
+
+ def add_column(self, table_name, name, field, keep_default=True):
"""
Adds the column 'name' to the table 'table_name'.
Uses the 'field' paramater, a django.db.models.fields.Field instance,
to generate the necessary sql
-
+
@param table_name: The name of the table to add the column to
@param name: The name of the column to add
@param field: The field to use
@@ -107,68 +161,82 @@ class DatabaseOperations(object):
qn(table_name),
sql,
)
- sql = 'ALTER TABLE %s ADD COLUMN %s;' % params
+ sql = self.add_column_string % params
self.execute(sql)
-
-
+
+ # Now, drop the default if we need to
+ if not keep_default and field.default:
+ field.default = NOT_PROVIDED
+ self.alter_column(table_name, name, field, explicit_name=False)
+
alter_string_set_type = 'ALTER COLUMN %(column)s TYPE %(type)s'
- alter_string_set_null = 'ALTER COLUMN %(column)s SET NOT NULL'
- alter_string_drop_null = 'ALTER COLUMN %(column)s DROP NOT NULL'
-
- def alter_column(self, table_name, name, field):
+ alter_string_set_null = 'ALTER COLUMN %(column)s DROP NOT NULL'
+ alter_string_drop_null = 'ALTER COLUMN %(column)s SET NOT NULL'
+ allows_combined_alters = True
+
+ def alter_column(self, table_name, name, field, explicit_name=True):
"""
Alters the given column name so it will match the given field.
Note that conversion between the two by the database must be possible.
-
+ Will not automatically add _id by default; to have this behavour, pass
+ explicit_name=False.
+
@param table_name: The name of the table to add the column to
@param name: The name of the column to alter
@param field: The new field definition to use
"""
-
+
# hook for the field to do any resolution prior to it's attributes being queried
if hasattr(field, 'south_init'):
field.south_init()
-
+
qn = connection.ops.quote_name
+ # Add _id or whatever if we need to
+ if not explicit_name:
+ field.set_attributes_from_name(name)
+ name = field.column
+
# First, change the type
params = {
"column": qn(name),
"type": field.db_type(),
}
- sqls = [self.alter_string_set_type % params]
-
-
+
+ # SQLs is a list of (SQL, values) pairs.
+ sqls = [(self.alter_string_set_type % params, [])]
+
# Next, set any default
- params = (
- qn(name),
- )
-
if not field.null and field.has_default():
default = field.get_default()
- if isinstance(default, basestring):
- default = "'%s'" % default
- params += ("SET DEFAULT %s",)
+ sqls.append(('ALTER COLUMN %s SET DEFAULT %%s ' % (qn(name),), [default]))
else:
- params += ("DROP DEFAULT",)
-
- sqls.append('ALTER COLUMN %s %s ' % params)
-
-
+ sqls.append(('ALTER COLUMN %s DROP DEFAULT' % (qn(name),), []))
+
+
# Next, nullity
params = {
"column": qn(name),
"type": field.db_type(),
}
if field.null:
- sqls.append(self.alter_string_drop_null % params)
+ sqls.append((self.alter_string_set_null % params, []))
else:
- sqls.append(self.alter_string_set_null % params)
-
-
+ sqls.append((self.alter_string_drop_null % params, []))
+
+
# TODO: Unique
-
- self.execute("ALTER TABLE %s %s;" % (qn(table_name), ", ".join(sqls)))
+
+ if self.allows_combined_alters:
+ sqls, values = zip(*sqls)
+ self.execute(
+ "ALTER TABLE %s %s;" % (qn(table_name), ", ".join(sqls)),
+ flatten(values),
+ )
+ else:
+ # Databases like e.g. MySQL don't like more than one alter at once.
+ for sql, values in sqls:
+ self.execute("ALTER TABLE %s %s;" % (qn(table_name), sql), values)
def column_sql(self, table_name, field_name, field, tablespace=''):
@@ -176,13 +244,13 @@ class DatabaseOperations(object):
Creates the SQL snippet for a column. Used by add_column and add_table.
"""
qn = connection.ops.quote_name
-
+
field.set_attributes_from_name(field_name)
-
+
# hook for the field to do any resolution prior to it's attributes being queried
if hasattr(field, 'south_init'):
field.south_init()
-
+
sql = field.db_type()
if sql:
field_output = [qn(field.column), sql]
@@ -190,26 +258,40 @@ class DatabaseOperations(object):
if field.primary_key:
field_output.append('PRIMARY KEY')
elif field.unique:
- field_output.append('UNIQUE')
-
+ # Instead of using UNIQUE, add a unique index with a predictable name
+ self.add_deferred_sql(
+ self.create_index_sql(
+ table_name,
+ [field.column],
+ unique = True,
+ db_tablespace = tablespace,
+ )
+ )
+
tablespace = field.db_tablespace or tablespace
if tablespace and connection.features.supports_tablespaces and field.unique:
# We must specify the index tablespace inline, because we
# won't be generating a CREATE INDEX statement for this field.
field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
-
+
sql = ' '.join(field_output)
sqlparams = ()
# if the field is "NOT NULL" and a default value is provided, create the column with it
# this allows the addition of a NOT NULL field to a table with existing rows
if not field.null and field.has_default():
default = field.get_default()
+ # If the default is a callable, then call it!
+ if callable(default):
+ default = default()
+ # Now do some very cheap quoting. TODO: Redesign return values to avoid this.
if isinstance(default, basestring):
default = "'%s'" % default.replace("'", "''")
+ elif isinstance(default, datetime.date):
+ default = "'%s'" % default
sql += " DEFAULT %s"
sqlparams = (default)
-
- if field.rel:
+
+ if field.rel and self.supports_foreign_keys:
self.add_deferred_sql(
self.foreign_key_sql(
table_name,
@@ -218,10 +300,10 @@ class DatabaseOperations(object):
field.rel.to._meta.get_field(field.rel.field_name).column
)
)
-
+
if field.db_index and not field.unique:
self.add_deferred_sql(self.create_index_sql(table_name, [field.column]))
-
+
if hasattr(field, 'post_create_sql'):
style = no_style()
for stmt in field.post_create_sql(style, table_name):
@@ -231,21 +313,26 @@ class DatabaseOperations(object):
return sql % sqlparams
else:
return None
-
+
+
+ supports_foreign_keys = True
+
def foreign_key_sql(self, from_table_name, from_column_name, to_table_name, to_column_name):
"""
Generates a full SQL statement to add a foreign key constraint
"""
+ qn = connection.ops.quote_name
constraint_name = '%s_refs_%s_%x' % (from_column_name, to_column_name, abs(hash((from_table_name, to_table_name))))
return 'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % (
- from_table_name,
- truncate_name(constraint_name, connection.ops.max_name_length()),
- from_column_name,
- to_table_name,
- to_column_name,
+ qn(from_table_name),
+ qn(truncate_name(constraint_name, connection.ops.max_name_length())),
+ qn(from_column_name),
+ qn(to_table_name),
+ qn(to_column_name),
connection.ops.deferrable_sql() # Django knows this
)
-
+
+
def create_index_name(self, table_name, column_names):
"""
Generate a unique name for the index
@@ -256,45 +343,55 @@ class DatabaseOperations(object):
return '%s_%s%s' % (table_name, column_names[0], index_unique_name)
+
def create_index_sql(self, table_name, column_names, unique=False, db_tablespace=''):
"""
Generates a create index statement on 'table_name' for a list of 'column_names'
"""
+ qn = connection.ops.quote_name
if not column_names:
print "No column names supplied on which to create an index"
return ''
-
+
if db_tablespace and connection.features.supports_tablespaces:
tablespace_sql = ' ' + connection.ops.tablespace_sql(db_tablespace)
else:
tablespace_sql = ''
-
+
index_name = self.create_index_name(table_name, column_names)
qn = connection.ops.quote_name
return 'CREATE %sINDEX %s ON %s (%s)%s;' % (
unique and 'UNIQUE ' or '',
- index_name,
- table_name,
+ qn(index_name),
+ qn(table_name),
','.join([qn(field) for field in column_names]),
tablespace_sql
- )
-
+ )
+
def create_index(self, table_name, column_names, unique=False, db_tablespace=''):
""" Executes a create index statement """
sql = self.create_index_sql(table_name, column_names, unique, db_tablespace)
self.execute(sql)
+ drop_index_string = 'DROP INDEX %(index_name)s'
+
def delete_index(self, table_name, column_names, db_tablespace=''):
"""
Deletes an index created with create_index.
This is possible using only columns due to the deterministic
index naming function which relies on column names.
"""
+ if isinstance(column_names, (str, unicode)):
+ column_names = [column_names]
name = self.create_index_name(table_name, column_names)
- sql = "DROP INDEX %s" % name
+ qn = connection.ops.quote_name
+ sql = self.drop_index_string % {"index_name": qn(name), "table_name": qn(table_name)}
self.execute(sql)
+ drop_index = alias('delete_index')
+
+ delete_column_string = 'ALTER TABLE %s DROP COLUMN %s CASCADE;'
def delete_column(self, table_name, name):
"""
@@ -302,7 +399,9 @@ class DatabaseOperations(object):
"""
qn = connection.ops.quote_name
params = (qn(table_name), qn(name))
- self.execute('ALTER TABLE %s DROP COLUMN %s CASCADE;' % params, [])
+ self.execute(self.delete_column_string % params, [])
+
+ drop_column = alias('delete_column')
def rename_column(self, table_name, old, new):
@@ -317,6 +416,8 @@ class DatabaseOperations(object):
Makes sure the following commands are inside a transaction.
Must be followed by a (commit|rollback)_transaction call.
"""
+ if self.dry_run:
+ return
transaction.commit_unless_managed()
transaction.enter_transaction_management()
transaction.managed(True)
@@ -327,6 +428,8 @@ class DatabaseOperations(object):
Commits the current transaction.
Must be preceded by a start_transaction call.
"""
+ if self.dry_run:
+ return
transaction.commit()
transaction.leave_transaction_management()
@@ -336,53 +439,67 @@ class DatabaseOperations(object):
Rolls back the current transaction.
Must be preceded by a start_transaction call.
"""
+ if self.dry_run:
+ return
transaction.rollback()
transaction.leave_transaction_management()
-
-
+
+
def send_create_signal(self, app_label, model_names):
+ self.pending_create_signals.append((app_label, model_names))
+
+
+ def send_pending_create_signals(self):
+ for (app_label, model_names) in self.pending_create_signals:
+ self.really_send_create_signal(app_label, model_names)
+ self.pending_create_signals = []
+
+
+ def really_send_create_signal(self, app_label, model_names):
"""
Sends a post_syncdb signal for the model specified.
-
+
If the model is not found (perhaps it's been deleted?),
no signal is sent.
-
+
TODO: The behavior of django.contrib.* apps seems flawed in that
they don't respect created_models. Rather, they blindly execute
over all models within the app sending the signal. This is a
patch we should push Django to make For now, this should work.
"""
+ if self.debug:
+ print " - Sending post_syncdb signal for %s: %s" % (app_label, model_names)
app = models.get_app(app_label)
if not app:
return
-
+
created_models = []
for model_name in model_names:
model = models.get_model(app_label, model_name)
if model:
created_models.append(model)
-
+
if created_models:
# syncdb defaults -- perhaps take these as options?
verbosity = 1
interactive = True
-
+
if hasattr(dispatcher, "send"):
dispatcher.send(signal=models.signals.post_syncdb, sender=app,
- app=app, created_models=created_models,
- verbosity=verbosity, interactive=interactive)
+ app=app, created_models=created_models,
+ verbosity=verbosity, interactive=interactive)
else:
models.signals.post_syncdb.send(sender=app,
- app=app, created_models=created_models,
- verbosity=verbosity, interactive=interactive)
-
+ app=app, created_models=created_models,
+ verbosity=verbosity, interactive=interactive)
+
def mock_model(self, model_name, db_table, db_tablespace='',
- pk_field_name='id', pk_field_type=models.AutoField,
- pk_field_kwargs={}):
+ pk_field_name='id', pk_field_type=models.AutoField,
+ pk_field_args=[], pk_field_kwargs={}):
"""
Generates a MockModel class that provides enough information
to be used by a foreign key/many-to-many relationship.
-
+
Migrations should prefer to use these rather than actual models
as models could get deleted over time, but these can remain in
migration files forever.
@@ -397,7 +514,7 @@ class DatabaseOperations(object):
if pk_field_type == models.AutoField:
pk_field_kwargs['primary_key'] = True
- self.pk = pk_field_type(**pk_field_kwargs)
+ self.pk = pk_field_type(*pk_field_args, **pk_field_kwargs)
self.pk.set_attributes_from_name(pk_field_name)
self.abstract = False
@@ -416,3 +533,11 @@ class DatabaseOperations(object):
MockModel._meta = MockOptions()
MockModel._meta.model = MockModel
return MockModel
+
+# Single-level flattening of lists
+def flatten(ls):
+ nl = []
+ for l in ls:
+ nl += l
+ return nl
+
diff --git a/apps/south/db/mysql.py b/apps/south/db/mysql.py
index c3659fc48..a05c0714d 100644
--- a/apps/south/db/mysql.py
+++ b/apps/south/db/mysql.py
@@ -1,5 +1,6 @@
from django.db import connection
+from django.conf import settings
from south.db import generic
class DatabaseOperations(generic.DatabaseOperations):
@@ -11,9 +12,20 @@ class DatabaseOperations(generic.DatabaseOperations):
alter_string_set_type = ''
alter_string_set_null = 'MODIFY %(column)s %(type)s NULL;'
alter_string_drop_null = 'MODIFY %(column)s %(type)s NOT NULL;'
+ drop_index_string = 'DROP INDEX %(index_name)s ON %(table_name)s'
+ allows_combined_alters = False
+ has_ddl_transactions = False
+
+ def execute(self, sql, params=[]):
+ if hasattr(settings, "DATABASE_STORAGE_ENGINE") and \
+ settings.DATABASE_STORAGE_ENGINE:
+ generic.DatabaseOperations.execute(self, "SET storage_engine=%s;" %
+ settings.DATABASE_STORAGE_ENGINE)
+ return generic.DatabaseOperations.execute(self, sql, params)
+ execute.__doc__ = generic.DatabaseOperations.execute.__doc__
def rename_column(self, table_name, old, new):
- if old == new:
+ if old == new or self.dry_run:
return []
qn = connection.ops.quote_name
@@ -27,17 +39,22 @@ class DatabaseOperations(generic.DatabaseOperations):
qn(table_name),
qn(old),
qn(new),
- "%s %s %s %s %s" % (
- rows[0][1],
- rows[0][2] == "YES" and "NULL" or "NOT NULL",
- rows[0][3] == "PRI" and "PRIMARY KEY" or "",
- rows[0][4] and "DEFAULT %s" % rows[0][4] or "",
- rows[0][5] or "",
- ),
+ rows[0][1],
+ rows[0][2] == "YES" and "NULL" or "NOT NULL",
+ rows[0][3] == "PRI" and "PRIMARY KEY" or "",
+ rows[0][4] and "DEFAULT " or "",
+ rows[0][4] and "%s" or "",
+ rows[0][5] or "",
)
- self.execute('ALTER TABLE %s CHANGE COLUMN %s %s %s;' % params)
-
-
+
+ sql = 'ALTER TABLE %s CHANGE COLUMN %s %s %s %s %s %s %s %s;' % params
+
+ if rows[0][4]:
+ self.execute(sql, (rows[0][4],))
+ else:
+ self.execute(sql)
+
+
def rename_table(self, old_table_name, table_name):
"""
Renames the table 'old_table_name' to 'table_name'.
@@ -47,4 +64,4 @@ class DatabaseOperations(generic.DatabaseOperations):
return
qn = connection.ops.quote_name
params = (qn(old_table_name), qn(table_name))
- self.execute('RENAME TABLE %s TO %s;' % params)
\ No newline at end of file
+ self.execute('RENAME TABLE %s TO %s;' % params)
diff --git a/apps/south/db/postgresql_psycopg2.py b/apps/south/db/postgresql_psycopg2.py
index 278eb3e9f..839b4b16f 100644
--- a/apps/south/db/postgresql_psycopg2.py
+++ b/apps/south/db/postgresql_psycopg2.py
@@ -16,6 +16,7 @@ class DatabaseOperations(generic.DatabaseOperations):
self.execute('ALTER TABLE %s RENAME COLUMN %s TO %s;' % params)
def rename_table(self, old_table_name, table_name):
+ "will rename the table and an associated ID sequence and primary key index"
# First, rename the table
generic.DatabaseOperations.rename_table(self, old_table_name, table_name)
# Then, try renaming the ID sequence
@@ -25,8 +26,27 @@ class DatabaseOperations(generic.DatabaseOperations):
try:
generic.DatabaseOperations.rename_table(self, old_table_name+"_id_seq", table_name+"_id_seq")
except:
- print " ~ No such sequence (ignoring error)"
+ if self.debug:
+ print " ~ No such sequence (ignoring error)"
self.rollback_transaction()
else:
self.commit_transaction()
- self.start_transaction()
\ No newline at end of file
+ self.start_transaction()
+
+ # Rename primary key index, will not rename other indices on
+ # the table that are used by django (e.g. foreign keys). Until
+ # figure out how, you need to do this yourself.
+ try:
+ generic.DatabaseOperations.rename_table(self, old_table_name+"_pkey", table_name+ "_pkey")
+ except:
+ if self.debug:
+ print " ~ No such primary key (ignoring error)"
+ self.rollback_transaction()
+ else:
+ self.commit_transaction()
+ self.start_transaction()
+
+
+ def rename_index(self, old_index_name, index_name):
+ "Rename an index individually"
+ generic.DatabaseOperations.rename_table(self, old_index_name, index_name)
diff --git a/apps/south/db/sql_server/__init__.py b/apps/south/db/sql_server/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/south/db/sql_server/pyodbc.py b/apps/south/db/sql_server/pyodbc.py
new file mode 100644
index 000000000..58c51669c
--- /dev/null
+++ b/apps/south/db/sql_server/pyodbc.py
@@ -0,0 +1,25 @@
+from django.db import connection
+from django.db.models.fields import *
+from south.db import generic
+
+class DatabaseOperations(generic.DatabaseOperations):
+ """
+ django-pyodbc (sql_server.pyodbc) implementation of database operations.
+ """
+
+ add_column_string = 'ALTER TABLE %s ADD %s;'
+ alter_string_set_type = 'ALTER COLUMN %(column)s %(type)s'
+ allows_combined_alters = False
+ delete_column_string = 'ALTER TABLE %s DROP COLUMN %s;'
+
+ def create_table(self, table_name, fields):
+ # Tweak stuff as needed
+ for name,f in fields:
+ if isinstance(f, BooleanField):
+ if f.default == True:
+ f.default = 1
+ if f.default == False:
+ f.default = 0
+
+ # Run
+ generic.DatabaseOperations.create_table(self, table_name, fields)
diff --git a/apps/south/db/sqlite3.py b/apps/south/db/sqlite3.py
index 6073b4d4c..1fac1b83d 100644
--- a/apps/south/db/sqlite3.py
+++ b/apps/south/db/sqlite3.py
@@ -8,5 +8,35 @@ class DatabaseOperations(generic.DatabaseOperations):
SQLite3 implementation of database operations.
"""
- def __init__(self):
- raise NotImplementedError("Support for SQLite3 is not yet complete.")
\ No newline at end of file
+ # SQLite ignores foreign key constraints. I wish I could.
+ supports_foreign_keys = False
+
+ # You can't add UNIQUE columns with an ALTER TABLE.
+ def add_column(self, table_name, name, field, *args, **kwds):
+ # Run ALTER TABLE with no unique column
+ unique, field._unique, field.db_index = field.unique, False, False
+ generic.DatabaseOperations.add_column(self, table_name, name, field, *args, **kwds)
+ # If it _was_ unique, make an index on it.
+ if unique:
+ self.create_index(table_name, [name], unique=True)
+
+ # SQLite doesn't have ALTER COLUMN
+ def alter_column(self, table_name, name, field, explicit_name=True):
+ """
+ Not supported under SQLite.
+ """
+ raise NotImplementedError("SQLite does not support altering columns.")
+
+ # Nor DROP COLUMN
+ def delete_column(self, table_name, name, field):
+ """
+ Not supported under SQLite.
+ """
+ raise NotImplementedError("SQLite does not support deleting columns.")
+
+ # Nor RENAME COLUMN
+ def rename_column(self, table_name, old, new):
+ """
+ Not supported under SQLite.
+ """
+ raise NotImplementedError("SQLite does not support renaming columns.")
\ No newline at end of file
diff --git a/apps/south/install/README b/apps/south/install/README
deleted file mode 100644
index 897b51d34..000000000
--- a/apps/south/install/README
+++ /dev/null
@@ -1 +0,0 @@
-To use this setup.py, make sure you checked out this trunk or branch into a directory called 'south', copy the setup.py into the directory above it, and off you go.
diff --git a/apps/south/install/setup.py b/apps/south/install/setup.py
deleted file mode 100755
index 6da3d4afc..000000000
--- a/apps/south/install/setup.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/python
-
-from setuptools import setup, find_packages
-
-setup(
- name='South',
- version='0.3',
- description='South: Migrations for Django',
- author='Andrew Godwin & Andy McCurdy',
- author_email='south@aeracode.org',
- url='http://south.aeracode.org/',
- packages=["south", "south.db", "south.management", "south.management.commands", "south.tests"],
-)
diff --git a/apps/south/management/commands/migrate.py b/apps/south/management/commands/migrate.py
index d2d69982f..1cc2a2908 100644
--- a/apps/south/management/commands/migrate.py
+++ b/apps/south/management/commands/migrate.py
@@ -16,11 +16,20 @@ class Command(BaseCommand):
help='Only runs or rolls back the migration specified, and none around it.'),
make_option('--fake', action='store_true', dest='fake', default=False,
help="Pretends to do the migrations, but doesn't actually execute them."),
+
+ make_option('--db-dry-run', action='store_true', dest='db_dry_run', default=False,
+ help="Doesn't execute the SQL generated by the db methods, and doesn't store a record that the migration(s) occurred. Useful to test migrations before applying them."),
)
+ if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]:
+ option_list += (
+ make_option('--verbosity', action='store', dest='verbosity', default='1',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+ )
help = "Runs migrations for all apps."
- def handle(self, app=None, target=None, skip=False, merge=False, only=False, backwards=False, fake=False, **options):
-
+ def handle(self, app=None, target=None, skip=False, merge=False, only=False, backwards=False, fake=False, db_dry_run=False, **options):
+
# Work out what the resolve mode is
resolve_mode = merge and "merge" or (skip and "skip" or None)
# Turn on db debugging
@@ -46,10 +55,14 @@ class Command(BaseCommand):
apps = [migration.get_app(app)]
else:
apps = migration.get_migrated_apps()
+ silent = options.get('verbosity', 0) == 0
for app in apps:
migration.migrate_app(
app,
resolve_mode = resolve_mode,
target_name = target,
fake = fake,
+ db_dry_run = db_dry_run,
+ silent = silent,
+ load_inital_data = True,
)
diff --git a/apps/south/management/commands/startmigration.py b/apps/south/management/commands/startmigration.py
index f52efe7aa..1a8da9977 100644
--- a/apps/south/management/commands/startmigration.py
+++ b/apps/south/management/commands/startmigration.py
@@ -3,6 +3,7 @@ from django.core.management.color import no_style
from django.db import models
from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
from django.contrib.contenttypes.generic import GenericRelation
+from django.db.models.fields import FieldDoesNotExist
from optparse import make_option
from south import migration
import sys
@@ -17,18 +18,23 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--model', action='append', dest='model_list', type='string',
help='Generate a Create Table migration for the specified model. Add multiple models to this migration with subsequent --model parameters.'),
+ make_option('--add-field', action='append', dest='field_list', type='string',
+ help='Generate an Add Column migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
make_option('--initial', action='store_true', dest='initial', default=False,
help='Generate the initial schema for the app.'),
)
help = "Creates a new template migration for the given app"
- def handle(self, app=None, name="", model_list=None, initial=False, **options):
+ def handle(self, app=None, name="", model_list=None, field_list=None, initial=False, **options):
# If model_list is None, then it's an empty list
model_list = model_list or []
+ # If field_list is None, then it's an empty list
+ field_list = field_list or []
+
# make sure --model and --all aren't both specified
- if initial and model_list:
+ if initial and (model_list or field_list):
print "You cannot use --initial and other options together"
return
@@ -67,7 +73,22 @@ class Command(BaseCommand):
return
models_to_migrate.append(model)
-
+
+ # See what fields need to be included
+ fields_to_add = []
+ for field_spec in field_list:
+ model_name, field_name = field_spec.split(".", 1)
+ model = models.get_model(app, model_name)
+ if not model:
+ print "Couldn't find model '%s' in app '%s'" % (model_name, app)
+ return
+ try:
+ field = model._meta.get_field(field_name)
+ except FieldDoesNotExist:
+ print "Model '%s' doesn't have a field '%s'" % (model_name, field_name)
+ return
+ fields_to_add.append((model, field_name, field))
+
# Make the migrations directory if it's not there
app_module_path = app_models_module.__name__.split('.')[0:-1]
try:
@@ -80,11 +101,15 @@ class Command(BaseCommand):
os.path.dirname(app_module.__file__),
"migrations",
)
+ # Make sure there's a migrations directory and __init__.py
if not os.path.isdir(migrations_dir):
print "Creating migrations directory at '%s'..." % migrations_dir
os.mkdir(migrations_dir)
+ init_path = os.path.join(migrations_dir, "__init__.py")
+ if not os.path.isfile(init_path):
# Touch the init py file
- open(os.path.join(migrations_dir, "__init__.py"), "w").close()
+ print "Creating __init__.py in '%s'..." % migrations_dir
+ open(init_path, "w").close()
# See what filename is next in line. We assume they use numbers.
migrations = migration.get_migration_names(migration.get_app(app))
highest_number = 0
@@ -102,23 +127,110 @@ class Command(BaseCommand):
)
# If there's a model, make the migration skeleton, else leave it bare
forwards, backwards = '', ''
+ if fields_to_add:
+ # First, do the added fields
+ for model, field_name, field in fields_to_add:
+ field_definition = generate_field_definition(model, field)
+
+ if isinstance(field, models.ManyToManyField):
+ # Make a mock model for each side
+ mock_model = "\n".join([
+ create_mock_model(model, " "),
+ create_mock_model(field.rel.to, " ")
+ ])
+ # And a field defn, that's actually a table creation
+ forwards += '''
+ # Mock Model
+%s
+ # Adding ManyToManyField '%s.%s'
+ db.create_table('%s', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('%s', models.ForeignKey(%s, null=False)),
+ ('%s', models.ForeignKey(%s, null=False))
+ )) ''' % (
+ mock_model,
+ model._meta.object_name,
+ field.name,
+ field.m2m_db_table(),
+ field.m2m_column_name()[:-3], # strip off the '_id' at the end
+ model._meta.object_name,
+ field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned
+ field.rel.to._meta.object_name
+ )
+ backwards += '''
+ # Dropping ManyToManyField '%s.%s'
+ db.drop_table('%s')''' % (
+ model._meta.object_name,
+ field.name,
+ field.m2m_db_table()
+ )
+ continue
+ elif field.rel: # ForeignKey, etc.
+ mock_model = create_mock_model(field.rel.to, " ")
+ field_definition = related_field_definition(field, field_definition)
+ else:
+ mock_model = None
+
+ # If we can't get it (inspect madness?) then insert placeholder
+ if not field_definition:
+ print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \
+ (model._meta.object_name, field.name)
+ field_definition = '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, f.name)
+
+ if mock_model:
+ forwards += '''
+ # Mock model
+%s
+ ''' % (mock_model)
+
+ forwards += '''
+ # Adding field '%s.%s'
+ db.add_column(%r, %r, %s)
+ ''' % (
+ model._meta.object_name,
+ field.name,
+ model._meta.db_table,
+ field.name,
+ field_definition,
+ )
+ backwards += '''
+ # Deleting field '%s.%s'
+ db.delete_column(%r, %r)
+ ''' % (
+ model._meta.object_name,
+ field.name,
+ model._meta.db_table,
+ field.column,
+ )
+
if models_to_migrate:
+ # Now, do the added models
for model in models_to_migrate:
table_name = model._meta.db_table
mock_models = []
fields = []
for f in model._meta.local_fields:
- # look up the field definition to see how this was created
+
+ # Look up the field definition to see how this was created
field_definition = generate_field_definition(model, f)
- if field_definition:
+
+ # If it's a OneToOneField, and ends in _ptr, just use it
+ if isinstance(f, models.OneToOneField) and f.name.endswith("_ptr"):
+ mock_models.append(create_mock_model(f.rel.to, " "))
+ field_definition = "models.OneToOneField(%s)" % f.rel.to.__name__
+
+ # It's probably normal then
+ elif field_definition:
if isinstance(f, models.ForeignKey):
- mock_models.append(create_mock_model(f.rel.to))
+ mock_models.append(create_mock_model(f.rel.to, " "))
field_definition = related_field_definition(f, field_definition)
-
+
+ # Oh noes, no defn found
else:
print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \
(model._meta.object_name, f.name)
+ print f, type(f)
field_definition = '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, f.name)
@@ -128,12 +240,12 @@ class Command(BaseCommand):
forwards += '''
# Mock Models
- %s
- ''' % "\n ".join(mock_models)
+%s
+ ''' % "\n".join(mock_models)
forwards += '''
# Model '%s'
- db.create_table('%s', (
+ db.create_table(%r, (
%s
))''' % (
model._meta.object_name,
@@ -155,11 +267,11 @@ class Command(BaseCommand):
if m.rel.through:
continue
- mock_models = [create_mock_model(model), create_mock_model(m.rel.to)]
+ mock_models = [create_mock_model(model, " "), create_mock_model(m.rel.to, " ")]
forwards += '''
# Mock Models
- %s
+%s
# M2M field '%s.%s'
db.create_table('%s', (
@@ -167,7 +279,7 @@ class Command(BaseCommand):
('%s', models.ForeignKey(%s, null=False)),
('%s', models.ForeignKey(%s, null=False))
)) ''' % (
- "\n ".join(mock_models),
+ "\n".join(mock_models),
model._meta.object_name,
m.name,
m.m2m_db_table(),
@@ -204,12 +316,20 @@ class Command(BaseCommand):
"','".join(model._meta.object_name for model in models_to_migrate)
)
- else:
+ # Try sniffing the encoding using PEP 0263's method
+ encoding = None
+ first_two_lines = inspect.getsourcelines(app_models_module)[0][:2]
+ for line in first_two_lines:
+ if re.search("coding[:=]\s*([-\w.]+)", line):
+ encoding = line
+
+ if (not forwards) and (not backwards):
forwards = '"Write your forwards migration here"'
backwards = '"Write your backwards migration here"'
fp = open(os.path.join(migrations_dir, new_filename), "w")
- fp.write("""
+ fp.write("""%s
from south.db import db
+from django.db import models
from %s.models import *
class Migration:
@@ -219,7 +339,7 @@ class Migration:
def backwards(self):
%s
-""" % ('.'.join(app_module_path), forwards, backwards))
+""" % (encoding or "", '.'.join(app_module_path), forwards, backwards))
fp.close()
print "Created %s." % new_filename
@@ -248,8 +368,11 @@ def generate_field_definition(model, field):
# the correct comment.
if test_field(stripped_definition):
return stripped_definition
-
- index = field_definition.index('#', index+1)
+
+ try:
+ index = field_definition.index('#', index+1)
+ except ValueError:
+ break
return field_definition
@@ -263,7 +386,7 @@ def generate_field_definition(model, field):
source = inspect.getsourcelines(model)
if not source:
raise Exception("Could not find source to model: '%s'" % (model.__name__))
-
+
# look for a line starting with the field name
start_field_re = re.compile(r'\s*%s\s*=\s*(.*)' % field.name)
for line in source[0]:
@@ -329,21 +452,40 @@ def related_field_definition(field, field_definition):
return field_definition
-def create_mock_model(model):
+def create_mock_model(model, indent=" "):
# produce a string representing the python syntax necessary for creating
# a mock model using the supplied real model
- if model._meta.pk.__class__.__module__ != 'django.db.models.fields':
+ if not model._meta.pk.__class__.__module__.startswith('django.db.models.fields'):
# we can fix this with some clever imports, but it doesn't seem necessary to
# spend time on just yet
- print "Can't generate a mock model for %s because it's primary key isn't a default django field" % model
+ print "Can't generate a mock model for %s because it's primary key isn't a default django field; it's type %s." % (model, model._meta.pk.__class__)
sys.exit()
- return "%s = db.mock_model(model_name='%s', db_table='%s', db_tablespace='%s', pk_field_name='%s', pk_field_type=models.%s)" % \
+ pk_field_args = []
+ pk_field_kwargs = {}
+ other_mocks = []
+ # If it's a OneToOneField or ForeignKey, take it's first arg
+ if model._meta.pk.__class__.__name__ in ["OneToOneField", "ForeignKey"]:
+ if model._meta.pk.rel.to == model:
+ pk_field_args += ["'self'"]
+ else:
+ pk_field_args += [model._meta.pk.rel.to._meta.object_name]
+ other_mocks += [model._meta.pk.rel.to]
+
+ # Perhaps it has a max_length set?
+ if model._meta.pk.max_length:
+ pk_field_kwargs["max_length"] = model._meta.pk.max_length
+
+ return "%s%s%s = db.mock_model(model_name='%s', db_table='%s', db_tablespace='%s', pk_field_name='%s', pk_field_type=models.%s, pk_field_args=[%s], pk_field_kwargs=%r)" % \
(
+ "\n".join([create_mock_model(m, indent) for m in other_mocks]+[""]),
+ indent,
model._meta.object_name,
model._meta.object_name,
model._meta.db_table,
model._meta.db_tablespace,
model._meta.pk.name,
- model._meta.pk.__class__.__name__
- )
\ No newline at end of file
+ model._meta.pk.__class__.__name__,
+ ", ".join(pk_field_args),
+ pk_field_kwargs,
+ )
diff --git a/apps/south/management/commands/syncdb.py b/apps/south/management/commands/syncdb.py
index 6ffc12015..7b160c27a 100644
--- a/apps/south/management/commands/syncdb.py
+++ b/apps/south/management/commands/syncdb.py
@@ -1,4 +1,4 @@
-from django.core.management.base import NoArgsCommand
+from django.core.management.base import NoArgsCommand, BaseCommand
from django.core.management.color import no_style
from django.utils.datastructures import SortedDict
from optparse import make_option
@@ -15,14 +15,17 @@ def get_app_name(app):
class Command(NoArgsCommand):
option_list = NoArgsCommand.option_list + (
- make_option('--verbosity', action='store', dest='verbosity', default='1',
- type='choice', choices=['0', '1', '2'],
- help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
make_option('--noinput', action='store_false', dest='interactive', default=True,
help='Tells Django to NOT prompt the user for input of any kind.'),
make_option('--migrate', action='store_true', dest='migrate', default=False,
help='Tells South to also perform migrations after the sync. Default for during testing, and other internal calls.'),
)
+ if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]:
+ option_list += (
+ make_option('--verbosity', action='store', dest='verbosity', default='1',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+ )
help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created, except those which use migrations."
def handle_noargs(self, **options):
@@ -37,8 +40,10 @@ class Command(NoArgsCommand):
else:
# This is a migrated app, leave it
apps_migrated.append(app_name)
+ verbosity = int(options.get('verbosity', 0))
# Run syncdb on only the ones needed
- print "Syncing..."
+ if verbosity > 0:
+ print "Syncing..."
old_installed, settings.INSTALLED_APPS = settings.INSTALLED_APPS, apps_needing_sync
old_app_store, cache.app_store = cache.app_store, SortedDict([
(k, v) for (k, v) in cache.app_store.items()
@@ -49,13 +54,17 @@ class Command(NoArgsCommand):
cache.app_store = old_app_store
# Migrate if needed
if options.get('migrate', True):
- print "Migrating..."
- management.call_command('migrate')
+ if verbosity > 0:
+ print "Migrating..."
+ management.call_command('migrate', **options)
# Be obvious about what we did
- print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync)
+ if verbosity > 0:
+ print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync)
if options.get('migrate', True):
- print "\nMigrated:\n - %s" % "\n - ".join(apps_migrated)
+ if verbosity > 0:
+ print "\nMigrated:\n - %s" % "\n - ".join(apps_migrated)
else:
- print "\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated)
- print "(use ./manage.py migrate to migrate these)"
+ if verbosity > 0:
+ print "\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated)
+ print "(use ./manage.py migrate to migrate these)"
diff --git a/apps/south/management/commands/test.py b/apps/south/management/commands/test.py
new file mode 100644
index 000000000..eef8f3157
--- /dev/null
+++ b/apps/south/management/commands/test.py
@@ -0,0 +1,12 @@
+from django.core import management
+from django.core.management.commands import test
+from django.core.management.commands import syncdb
+
+class Command(test.Command):
+
+ def handle(self, *args, **kwargs):
+ # point at the core syncdb command when creating tests
+ # tests should always be up to date with the most recent model structure
+ management.get_commands()
+ management._commands['syncdb'] = 'django.core'
+ super(Command, self).handle(*args, **kwargs)
\ No newline at end of file
diff --git a/apps/south/migration.py b/apps/south/migration.py
index c650c5475..6452442d3 100644
--- a/apps/south/migration.py
+++ b/apps/south/migration.py
@@ -2,8 +2,11 @@
import datetime
import os
import sys
+import traceback
from django.conf import settings
from django.db import models
+from django.core.exceptions import ImproperlyConfigured
+from django.core.management import call_command
from models import MigrationHistory
from south.db import db
@@ -57,7 +60,7 @@ def get_migration_names(app):
return sorted([
filename[:-3]
for filename in os.listdir(os.path.dirname(app.__file__))
- if filename.endswith(".py") and filename != "__init__.py"
+ if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith(".")
])
@@ -77,7 +80,9 @@ def get_migration(app, name):
module = __import__(app.__name__ + "." + name, '', '', ['Migration'])
return module.Migration
except ImportError:
- raise ValueError("Migration %s:%s does not exist." % (get_app_name(app), name))
+ print " ! Migration %s:%s probably doesn't exist." % (get_app_name(app), name)
+ print " - Traceback:"
+ raise
def all_migrations():
@@ -201,61 +206,116 @@ def needed_before_backwards(tree, app, name, sameapp=True):
return remove_duplicates(needed)
-def run_forwards(app, migrations, fake=False, silent=False):
+def run_migrations(toprint, torun, recorder, app, migrations, fake=False, db_dry_run=False, silent=False):
"""
Runs the specified migrations forwards, in order.
"""
for migration in migrations:
app_name = get_app_name(app)
if not silent:
- print " > %s: %s" % (app_name, migration)
+ print toprint % (app_name, migration)
klass = get_migration(app, migration)
+
if fake:
if not silent:
print " (faked)"
else:
- db.start_transaction()
+
+ # If the database doesn't support running DDL inside a transaction
+ # *cough*MySQL*cough* then do a dry run first.
+ if not db.has_ddl_transactions:
+ db.dry_run = True
+ db.debug, old_debug = False, db.debug
+ try:
+ getattr(klass(), torun)()
+ except:
+ traceback.print_exc()
+ print " ! Error found during dry run of migration! Aborting."
+ return False
+ db.debug = old_debug
+ db.clear_run_data()
+
+ db.dry_run = bool(db_dry_run)
+
+ if db.has_ddl_transactions:
+ db.start_transaction()
try:
- klass().forwards()
+ getattr(klass(), torun)()
db.execute_deferred_sql()
except:
- db.rollback_transaction()
- raise
+ if db.has_ddl_transactions:
+ db.rollback_transaction()
+ raise
+ else:
+ traceback.print_exc()
+ print " ! Error found during real run of migration! Aborting."
+ print
+ print " ! Since you have a database that does not support running"
+ print " ! schema-altering statements in transactions, we have had to"
+ print " ! leave it in an interim state between migrations."
+ if torun == "forwards":
+ print
+ print " ! You *might* be able to recover with:"
+ db.debug = db.dry_run = True
+ klass().backwards()
+ print
+ print " ! The South developers regret this has happened, and would"
+ print " ! like to gently persuade you to consider a slightly"
+ print " ! easier-to-deal-with DBMS."
+ return False
else:
- db.commit_transaction()
+ if db.has_ddl_transactions:
+ db.commit_transaction()
+
+ if not db_dry_run:
+ # Record us as having done this
+ recorder(app_name, migration)
+
+
+def run_forwards(app, migrations, fake=False, db_dry_run=False, silent=False):
+ """
+ Runs the specified migrations forwards, in order.
+ """
+
+ def record(app_name, migration):
# Record us as having done this
record = MigrationHistory.for_migration(app_name, migration)
record.applied = datetime.datetime.utcnow()
record.save()
-
-
-def run_backwards(app, migrations, ignore=[], fake=False, silent=False):
+
+ return run_migrations(
+ toprint = " > %s: %s",
+ torun = "forwards",
+ recorder = record,
+ app = app,
+ migrations = migrations,
+ fake = fake,
+ db_dry_run = db_dry_run,
+ silent = silent,
+ )
+
+
+def run_backwards(app, migrations, ignore=[], fake=False, db_dry_run=False, silent=False):
"""
Runs the specified migrations backwards, in order, skipping those
migrations in 'ignore'.
"""
- for migration in migrations:
- if migration not in ignore:
- app_name = get_app_name(app)
- if not silent:
- print " < %s: %s" % (app_name, migration)
- klass = get_migration(app, migration)
- if fake:
- if not silent:
- print " (faked)"
- else:
- db.start_transaction()
- try:
- klass().backwards()
- db.execute_deferred_sql()
- except:
- db.rollback_transaction()
- raise
- else:
- db.commit_transaction()
- # Record us as having not done this
- record = MigrationHistory.for_migration(app_name, migration)
- record.delete()
+
+ def record(app_name, migration):
+ # Record us as having not done this
+ record = MigrationHistory.for_migration(app_name, migration)
+ record.delete()
+
+ return run_migrations(
+ toprint = " < %s: %s",
+ torun = "backwards",
+ recorder = record,
+ app = app,
+ migrations = [x for x in migrations if x not in ignore],
+ fake = fake,
+ db_dry_run = db_dry_run,
+ silent = silent,
+ )
def right_side_of(x, y):
@@ -291,7 +351,7 @@ def backwards_problems(tree, backwards, done, silent=False):
return problems
-def migrate_app(app, target_name=None, resolve_mode=None, fake=False, yes=False, silent=False):
+def migrate_app(app, target_name=None, resolve_mode=None, fake=False, db_dry_run=False, yes=False, silent=False, load_inital_data=False):
app_name = get_app_name(app)
@@ -310,6 +370,12 @@ def migrate_app(app, target_name=None, resolve_mode=None, fake=False, yes=False,
tree = dependency_tree()
migrations = get_migration_names(app)
+ # If there aren't any, quit quizically
+ if not migrations:
+ if not silent:
+ print "? You have no migrations for the '%s' app. You might want some." % app_name
+ return
+
if target_name not in migrations and target_name not in ["zero", None]:
matches = [x for x in migrations if x.startswith(target_name)]
if len(matches) == 1:
@@ -331,7 +397,15 @@ def migrate_app(app, target_name=None, resolve_mode=None, fake=False, yes=False,
return
# Check there's no strange ones in the database
- ghost_migrations = [m for m in MigrationHistory.objects.filter(applied__isnull = False) if get_app(m.app_name) not in tree or m.migration not in tree[get_app(m.app_name)]]
+ ghost_migrations = []
+ for m in MigrationHistory.objects.filter(applied__isnull = False):
+ try:
+ if get_app(m.app_name) not in tree or m.migration not in tree[get_app(m.app_name)]:
+ ghost_migrations.append(m)
+ except ImproperlyConfigured:
+ pass
+
+
if ghost_migrations:
if not silent:
print " ! These migrations are in the database but not on disk:"
@@ -361,7 +435,12 @@ def migrate_app(app, target_name=None, resolve_mode=None, fake=False, yes=False,
backwards = []
# Get the list of currently applied migrations from the db
- current_migrations = [(get_app(m.app_name), m.migration) for m in MigrationHistory.objects.filter(applied__isnull = False)]
+ current_migrations = []
+ for m in MigrationHistory.objects.filter(applied__isnull = False):
+ try:
+ current_migrations.append((get_app(m.app_name), m.migration))
+ except ImproperlyConfigured:
+ pass
direction = None
bad = False
@@ -416,15 +495,34 @@ def migrate_app(app, target_name=None, resolve_mode=None, fake=False, yes=False,
if direction == 1:
if not silent:
print " - Migrating forwards to %s." % target_name
- for mapp, mname in forwards:
- if (mapp, mname) not in current_migrations:
- run_forwards(mapp, [mname], fake=fake, silent=silent)
+ try:
+ for mapp, mname in forwards:
+ if (mapp, mname) not in current_migrations:
+ result = run_forwards(mapp, [mname], fake=fake, db_dry_run=db_dry_run, silent=silent)
+ if result is False: # The migrations errored, but nicely.
+ return
+ finally:
+ # Call any pending post_syncdb signals
+ db.send_pending_create_signals()
+ # Now load initial data, only if we're really doing things and ended up at current
+ if not fake and not db_dry_run and load_inital_data and target_name == migrations[-1]:
+ print " - Loading initial data for %s." % app_name
+ # Override Django's get_apps call temporarily to only load from the
+ # current app
+ old_get_apps, models.get_apps = (
+ models.get_apps,
+ lambda: [models.get_app(get_app_name(app))],
+ )
+ # Load the initial fixture
+ call_command('loaddata', 'initial_data', verbosity=1)
+ # Un-override
+ models.get_apps = old_get_apps
elif direction == -1:
if not silent:
print " - Migrating backwards to just after %s." % target_name
for mapp, mname in backwards:
if (mapp, mname) in current_migrations:
- run_backwards(mapp, [mname], fake=fake, silent=silent)
+ run_backwards(mapp, [mname], fake=fake, db_dry_run=db_dry_run, silent=silent)
else:
if not silent:
- print "- Nothing to migrate."
\ No newline at end of file
+ print "- Nothing to migrate."
diff --git a/apps/south/setup.py b/apps/south/setup.py
new file mode 100755
index 000000000..9e0958364
--- /dev/null
+++ b/apps/south/setup.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python
+
+from setuptools import setup, find_packages
+
+setup(
+ name='South',
+ version='0.4',
+ description='South: Migrations for Django',
+ long_description='South is an intelligent database migrations library for the Django web framework. It is database-independent and DVCS-friendly, as well as a whole host of other features.',
+ author='Andrew Godwin & Andy McCurdy',
+ author_email='south@aeracode.org',
+ url='http://south.aeracode.org/',
+ download_url='http://south.aeracode.org/wiki/Download',
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: Django",
+ "Intended Audience :: Developers",
+ "Intended Audience :: System Administrators",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development"
+ ],
+ packages=["south", "south.db", "south.management", "south.management.commands", "south.tests", "south.tests.fakeapp", "south.tests.fakeapp.migrations"],
+ package_dir = {"south" : ""},
+)
diff --git a/apps/south/tests/db.py b/apps/south/tests/db.py
index c47f02198..b7bb14541 100644
--- a/apps/south/tests/db.py
+++ b/apps/south/tests/db.py
@@ -21,6 +21,7 @@ class TestOperations(unittest.TestCase):
def setUp(self):
db.debug = False
+ db.clear_deferred_sql()
def test_create(self):
"""
@@ -45,7 +46,7 @@ class TestOperations(unittest.TestCase):
db.rollback_transaction()
db.start_transaction()
# Remove the table
- db.delete_table("test1")
+ db.drop_table("test1")
# Make sure it went
try:
cursor.execute("SELECT * FROM test1")
@@ -63,6 +64,23 @@ class TestOperations(unittest.TestCase):
pass
db.rollback_transaction()
+ def test_foreign_keys(self):
+ """
+ Tests foreign key creation, especially uppercase (see #61)
+ """
+ Test = db.mock_model(model_name='Test', db_table='test5a',
+ db_tablespace='', pk_field_name='ID',
+ pk_field_type=models.AutoField, pk_field_args=[])
+ cursor = connection.cursor()
+ db.start_transaction()
+ db.create_table("test5a", [('ID', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True))])
+ db.create_table("test5b", [
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('UNIQUE', models.ForeignKey(Test)),
+ ])
+ db.execute_deferred_sql()
+ db.rollback_transaction()
+
def test_rename(self):
"""
Test column renaming
@@ -80,4 +98,88 @@ class TestOperations(unittest.TestCase):
self.fail("Just-renamed column could be selected!")
except:
pass
- db.rollback_transaction()
\ No newline at end of file
+ db.rollback_transaction()
+ db.delete_table("test2")
+
+ def test_dry_rename(self):
+ """
+ Test column renaming while --dry-run is turned on (should do nothing)
+ See ticket #65
+ """
+ cursor = connection.cursor()
+ db.create_table("test2", [('spam', models.BooleanField(default=False))])
+ db.start_transaction()
+ # Make sure we can select the column
+ cursor.execute("SELECT spam FROM test2")
+ # Rename it
+ db.dry_run = True
+ db.rename_column("test2", "spam", "eggs")
+ db.dry_run = False
+ cursor.execute("SELECT spam FROM test2")
+ try:
+ cursor.execute("SELECT eggs FROM test2")
+ self.fail("Dry-renamed new column could be selected!")
+ except:
+ pass
+ db.rollback_transaction()
+ db.delete_table("test2")
+
+ def test_table_rename(self):
+ """
+ Test column renaming
+ """
+ cursor = connection.cursor()
+ db.create_table("testtr", [('spam', models.BooleanField(default=False))])
+ db.start_transaction()
+ # Make sure we can select the column
+ cursor.execute("SELECT spam FROM testtr")
+ # Rename it
+ db.rename_table("testtr", "testtr2")
+ cursor.execute("SELECT spam FROM testtr2")
+ try:
+ cursor.execute("SELECT spam FROM testtr")
+ self.fail("Just-renamed column could be selected!")
+ except:
+ pass
+ db.rollback_transaction()
+ db.delete_table("testtr2")
+
+ def test_index(self):
+ """
+ Test the index operations
+ """
+ db.create_table("test3", [
+ ('SELECT', models.BooleanField(default=False)),
+ ('eggs', models.IntegerField(unique=True)),
+ ])
+ db.execute_deferred_sql()
+ db.start_transaction()
+ # Add an index on that column
+ db.create_index("test3", ["SELECT"])
+ # Add another index on two columns
+ db.create_index("test3", ["SELECT", "eggs"])
+ # Delete them both
+ db.delete_index("test3", ["SELECT"])
+ db.delete_index("test3", ["SELECT", "eggs"])
+ # Delete the unique index
+ db.delete_index("test3", ["eggs"])
+ db.rollback_transaction()
+ db.delete_table("test3")
+
+ def test_alter(self):
+ """
+ Test altering columns/tables
+ """
+ db.create_table("test4", [
+ ('spam', models.BooleanField(default=False)),
+ ('eggs', models.IntegerField()),
+ ])
+ db.start_transaction()
+ # Add a column
+ db.add_column("test4", "add1", models.IntegerField(default=3), keep_default=False)
+ # Add a FK with keep_default=False (#69)
+ User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={})
+ db.add_column("test4", "user", models.ForeignKey(User), keep_default=False)
+
+ db.rollback_transaction()
+ db.delete_table("test4")
\ No newline at end of file
diff --git a/apps/south/tests/fakeapp/migrations/0003_alter_spam.py b/apps/south/tests/fakeapp/migrations/0003_alter_spam.py
new file mode 100644
index 000000000..3a9aea49e
--- /dev/null
+++ b/apps/south/tests/fakeapp/migrations/0003_alter_spam.py
@@ -0,0 +1,12 @@
+from south.db import db
+from django.db import models
+
+class Migration:
+
+ def forwards(self):
+
+ db.alter_column("southtest_spam", 'name', models.CharField(max_length=255, null=True))
+
+ def backwards(self):
+
+ db.alter_column("southtest_spam", 'name', models.CharField(max_length=255))
diff --git a/apps/south/tests/logic.py b/apps/south/tests/logic.py
index dfb441eed..862c52dce 100644
--- a/apps/south/tests/logic.py
+++ b/apps/south/tests/logic.py
@@ -117,7 +117,7 @@ class TestMigrationLogic(unittest.TestCase):
app = self.create_test_app()
self.assertEqual(
- ["0001_spam", "0002_eggs"],
+ ["0001_spam", "0002_eggs", "0003_alter_spam"],
migration.get_migration_names(app),
)
@@ -129,9 +129,10 @@ class TestMigrationLogic(unittest.TestCase):
# Can't use vanilla import, modules beginning with numbers aren't in grammar
M1 = __import__("fakeapp.migrations.0001_spam", {}, {}, ['Migration']).Migration
M2 = __import__("fakeapp.migrations.0002_eggs", {}, {}, ['Migration']).Migration
+ M3 = __import__("fakeapp.migrations.0003_alter_spam", {}, {}, ['Migration']).Migration
self.assertEqual(
- [M1, M2],
+ [M1, M2, M3],
list(migration.get_migration_classes(app)),
)
@@ -147,7 +148,7 @@ class TestMigrationLogic(unittest.TestCase):
self.assertEqual(M1, migration.get_migration(app, "0001_spam"))
self.assertEqual(M2, migration.get_migration(app, "0002_eggs"))
- self.assertRaises(ValueError, migration.get_migration, app, "0001_jam")
+ self.assertRaises((ImportError, ValueError), migration.get_migration, app, "0001_jam")
def test_all_migrations(self):
@@ -158,6 +159,7 @@ class TestMigrationLogic(unittest.TestCase):
{app: {
"0001_spam": migration.get_migration(app, "0001_spam"),
"0002_eggs": migration.get_migration(app, "0002_eggs"),
+ "0003_alter_spam": migration.get_migration(app, "0003_alter_spam"),
}},
migration.all_migrations(),
)
@@ -186,6 +188,7 @@ class TestMigrationLogic(unittest.TestCase):
(
(u"fakeapp", u"0001_spam"),
(u"fakeapp", u"0002_eggs"),
+ (u"fakeapp", u"0003_alter_spam"),
),
migration.MigrationHistory.objects.values_list("app_name", "migration"),
)
@@ -241,13 +244,49 @@ class TestMigrationLogic(unittest.TestCase):
(
(u"fakeapp", u"0001_spam"),
(u"fakeapp", u"0002_eggs"),
+ (u"fakeapp", u"0003_alter_spam"),
),
migration.MigrationHistory.objects.values_list("app_name", "migration"),
)
# Now roll them backwards
+ migration.migrate_app(app, target_name="0002", resolve_mode=None, fake=False, silent=True)
migration.migrate_app(app, target_name="0001", resolve_mode=None, fake=True, silent=True)
migration.migrate_app(app, target_name="zero", resolve_mode=None, fake=False, silent=True)
# Finish with none
+ self.assertEqual(list(migration.MigrationHistory.objects.all()), [])
+
+ def test_alter_column_null(self):
+ def null_ok():
+ from django.db import connection, transaction
+ # the DBAPI introspection module fails on postgres NULLs.
+ cursor = connection.cursor()
+ try:
+ cursor.execute("INSERT INTO southtest_spam (id, weight, expires, name) VALUES (100, 10.1, now(), NULL);")
+ except:
+ transaction.rollback()
+ return False
+ else:
+ cursor.execute("DELETE FROM southtest_spam")
+ transaction.commit()
+ return True
+
+ app = migration.get_app("fakeapp")
+ self.assertEqual(list(migration.MigrationHistory.objects.all()), [])
+
+ # by default name is NOT NULL
+ migration.migrate_app(app, target_name="0002", resolve_mode=None, fake=False, silent=True)
+ self.failIf(null_ok())
+
+ # after 0003, it should be NULL
+ migration.migrate_app(app, target_name="0003", resolve_mode=None, fake=False, silent=True)
+ self.assert_(null_ok())
+
+ # make sure it is NOT NULL again
+ migration.migrate_app(app, target_name="0002", resolve_mode=None, fake=False, silent=True)
+ self.failIf(null_ok(), 'name not null after migration')
+
+ # finish with no migrations, otherwise other tests fail...
+ migration.migrate_app(app, target_name="zero", resolve_mode=None, fake=False, silent=True)
self.assertEqual(list(migration.MigrationHistory.objects.all()), [])
\ No newline at end of file
diff --git a/wolnelektury/templates/catalogue/book_detail.html b/wolnelektury/templates/catalogue/book_detail.html
index f4805b9a6..d94ebe9be 100644
--- a/wolnelektury/templates/catalogue/book_detail.html
+++ b/wolnelektury/templates/catalogue/book_detail.html
@@ -94,9 +94,14 @@
{{ tag }}
{% endfor %}
+
+ W innych miejscach
+
diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html
index 5bcab78a8..f2e3c2430 100644
--- a/wolnelektury/templates/catalogue/tagged_object_list.html
+++ b/wolnelektury/templates/catalogue/tagged_object_list.html
@@ -30,6 +30,9 @@
Pobierz wszystkie ksiÄ
żki z tej póÅki
{% endif %}
+ {% if last_tag.gazeta_link %}
+
Przeczytaj omówienia lektur w Lektury.Gazeta.pl
+ {% endif %}
{% for book in object_list %}
-
--
2.20.1
From d0c863efe1e0e2b0b2d41c6482c42733a1b57df4 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Tue, 21 Apr 2009 00:20:24 +0200
Subject: [PATCH 04/16] =?utf8?q?Zmiana=20adresu=20URL=20z=20aktualno=C5=9B?=
=?utf8?q?ciami=20i=20konwertowanie=20adresu=20z=20unicode=20do=20str=20w?=
=?utf8?q?=20tagu=20latest=5Fblog=5Fposts.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit
---
apps/catalogue/templatetags/catalogue_tags.py | 10 +++++-----
wolnelektury/templates/catalogue/main_page.html | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py
index 3c34e8cdd..d3f608ac9 100644
--- a/apps/catalogue/templatetags/catalogue_tags.py
+++ b/apps/catalogue/templatetags/catalogue_tags.py
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
+import feedparser
+import datetime
+
from django import template
from django.template import Node, Variable
from django.utils.encoding import smart_str
@@ -190,12 +193,9 @@ class CatalogueURLNode(Node):
@register.inclusion_tag('catalogue/latest_blog_posts.html')
-def latest_blog_posts(feed_url, posts_to_show=5):
- import feedparser
- import datetime
-
+def latest_blog_posts(feed_url, posts_to_show=5):
try:
- feed = feedparser.parse(feed_url)
+ feed = feedparser.parse(str(feed_url))
posts = []
for i in range(posts_to_show):
pub_date = feed['entries'][i].updated_parsed
diff --git a/wolnelektury/templates/catalogue/main_page.html b/wolnelektury/templates/catalogue/main_page.html
index 43a3636f7..af48f8402 100644
--- a/wolnelektury/templates/catalogue/main_page.html
+++ b/wolnelektury/templates/catalogue/main_page.html
@@ -253,7 +253,7 @@
AktualnoÅci
{% cache 1800 latest-blog-posts %}
- {% latest_blog_posts "http://www.nowoczesnapolska.org.pl/feed/?s=Wolne%20Lektury" %}
+ {% latest_blog_posts "http://www.nowoczesnapolska.org.pl/tematy/wolne-lektury/feed/" %}
{% endcache %}
Zobacz nasz blog â
--
2.20.1
From b3a2a942879cf81025be9cbfcb924b3ffca038c0 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 21:54:36 +0200
Subject: [PATCH 05/16] =?utf8?q?Zmiana=20tytu=C5=82=C3=B3w=20link=C3=B3w?=
=?utf8?q?=20do=20Lektury.Gazeta.pl.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit
---
wolnelektury/templates/catalogue/book_detail.html | 2 +-
wolnelektury/templates/catalogue/tagged_object_list.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/wolnelektury/templates/catalogue/book_detail.html b/wolnelektury/templates/catalogue/book_detail.html
index d94ebe9be..a31d8c43f 100644
--- a/wolnelektury/templates/catalogue/book_detail.html
+++ b/wolnelektury/templates/catalogue/book_detail.html
@@ -100,7 +100,7 @@
- Lektura na wiki projektu
- Lektura w CBN Polona
{% if book.gazeta_link %}
- - Omówienie lektury w Lektury.Gazeta.pl
+ - Lektura w Lektury.Gazeta.pl
{% endif %}
diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html
index f2e3c2430..d1d0b56b6 100644
--- a/wolnelektury/templates/catalogue/tagged_object_list.html
+++ b/wolnelektury/templates/catalogue/tagged_object_list.html
@@ -31,7 +31,7 @@
{% endif %}
{% if last_tag.gazeta_link %}
- Przeczytaj omówienia lektur w Lektury.Gazeta.pl
+ {{ last_tag }} w Lektury.Gazeta.pl
{% endif %}
{% for book in object_list %}
--
2.20.1
From 1e5176481313c3129d2f353993dbb721b21ef232 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 22:21:51 +0200
Subject: [PATCH 06/16] Added scripts and data for importing links from gazeta
and wikipedia.
---
scripts/gazeta-links | 60 ++++++++++++++++++++++++++++++++
scripts/import_links.py | 36 +++++++++++++++++++
scripts/wiki-links | 76 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 172 insertions(+)
create mode 100644 scripts/gazeta-links
create mode 100644 scripts/import_links.py
create mode 100644 scripts/wiki-links
diff --git a/scripts/gazeta-links b/scripts/gazeta-links
new file mode 100644
index 000000000..922969bec
--- /dev/null
+++ b/scripts/gazeta-links
@@ -0,0 +1,60 @@
+aleksander-fredro http://aleksander_fredro.lektury.gazeta.pl/lektury/1,90376,5202533,Aleksander_Fredro_-_biografia_autora.html
+johann-wolfgang-von-goethe http://goethe_johann_wolfgang.lektury.gazeta.pl/lektury/1,89634,5132561,Johann_Wolfgang_Goethe_-_biografia.html
+jan-kochanowski http://jan_kochanowski.lektury.gazeta.pl/lektury/1,90379,5202621,Jan_Kochanowski_-_biografia.html
+maria-konopnicka http://konopnicka_maria.lektury.gazeta.pl/lektury/1,89639,5132605,Maria_Konopnicka_-_biografia.html
+ignacy-krasicki http://krasicki_ignacy.lektury.gazeta.pl/lektury/1,89642,5132887,Ignacy_Krasicki_-_biografia.html
+zygmunt-krasinski http://zygmunt_krasinski.lektury.gazeta.pl/lektury/1,90374,5202485,Zygmunt_Krasinski_-_biografia.html
+boleslaw-lesmian http://lesmian_boleslaw.lektury.gazeta.pl/lektury/1,89643,5132898,Boleslaw_Lesmian_-_biografia_.html
+adam-mickiewicz http://mickiewicz_adam.lektury.gazeta.pl/lektury/1,89644,5053153,Adam_Mickiewicz_-_biografia_.html
+jan-poguelin-moliere http://molier.lektury.gazeta.pl/lektury/1,93571,5609143,Molier___biografia.html
+jan-andrzej-morsztyn http://jan_andrzej_morsztyn.lektury.gazeta.pl/lektury/1,93983,5660227,Jan_Andrzej_Morsztyn___biografia.html
+boleslaw-prus http://prus_boleslaw.lektury.gazeta.pl/lektury/1,89649,5133153,Boleslaw_Prus_-_biografia_.html
+wladyslaw-stanislaw-reymont http://reymont_wladyslaw_stanislaw.lektury.gazeta.pl/lektury/1,89650,5133327,Wladyslaw_Reymont_-_biografia_.html
+henryk-sienkiewicz http://sienkiewicz_henryk.lektury.gazeta.pl/lektury/1,89652,5133345,Henryk_Sienkiewicz_-_biografia_.html
+juliusz-slowacki http://slowacki_juliusz.lektury.gazeta.pl/lektury/1,89653,5133353,Juliusz_Slowacki_-_biografia.html
+sofokles http://sofokles.lektury.gazeta.pl/lektury/1,89654,5133364,Sofokles_-_biografia.html
+stanislaw-wyspianski http://stanislaw_wyspianski.lektury.gazeta.pl/lektury/1,93611,5613067,Stanislaw_Wyspianski___biografia.html
+gabriela-zapolska http://zapolska_gabriela.lektury.gazeta.pl/lektury/1,89656,5133395,Gabriela_Zapolska___biografia.html
+stefan-zeromski http://zeromski_stefan.lektury.gazeta.pl/lektury/1,89657,5710923,Stefan_Zeromski___biografia.html
+antygona http://antygona.lektury.gazeta.pl/lektury/1,89451,5099628,Antygona_.html
+bajki-i-przypowiesci http://bajki.lektury.gazeta.pl/lektury/1,89472,5102409,Bajki.html
+ballady-i-romanse http://ballady_i_romanse.lektury.gazeta.pl/lektury/1,89069,5041258,Ballady_i_romanse_.html
+chlopi http://chlopi.lektury.gazeta.pl/lektury/1,89425,5095761,Chlopi_.html
+cierpienia-mlodego-wertera http://cierpienia_mlodego_wertera.lektury.gazeta.pl/lektury/1,89169,5056058,Cierpienia_mlodego_Wertera_.html
+cuda-milosci-przebog-jak-zyje-serca-juz-nie-majac http://cuda_milosci.lektury.gazeta.pl/lektury/1,93979,5660231,Cuda_milosci.html
+do-trupa http://do_trupa.lektury.gazeta.pl/lektury/1,93982,5662283,Do_trupa.html
+doktor-piotr http://doktor_piotr.lektury.gazeta.pl/lektury/1,88823,4998689,Doktor_Piotr.html
+dusiolek http://dusiolek.lektury.gazeta.pl/lektury/1,88877,5010900,Dusiolek_.html
+dziewczyna http://dziewczyna.lektury.gazeta.pl/lektury/1,88889,5011210,Dziewczyna_.html
+lalka http://lalka.lektury.gazeta.pl/lektury/1,90217,5183302,Lalka.html
+latarnik http://latarnik.lektury.gazeta.pl/lektury/1,90267,5187298,Latarnik.html
+ludzie-bezdomni http://ludzie_bezdomni.lektury.gazeta.pl/lektury/1,88876,5010793,Ludzie_bezdomni_.html
+moralnosc-pani-dulskiej http://moralnosc_pani_dulskiej.lektury.gazeta.pl/lektury/1,88833,5000418,Moralnosc_pani_Dulskiej.html
+nasza-szkapa http://nasza_szkapa.lektury.gazeta.pl/lektury/1,88532,4965124,Nasza_szkapa.html
+nie-boska-komedia http://nie_boska_komedia.lektury.gazeta.pl/lektury/1,90331,5192068,Nie-Boska_komedia.html
+niestatek-oczy-sa-ogien-czolo-jest-zwierciadlem http://niestatek_i.lektury.gazeta.pl/lektury/1,93981,5662285,Niestatek_I.html
+niestatek-predzej-kto-wiatr-w-wor-zamknie-predzej-i-promieni http://niestatek_ii.lektury.gazeta.pl/lektury/1,93980,5662286,Niestatek_II.html
+oda-do-mlodosci http://oda_do_mlodosci.lektury.gazeta.pl/lektury/1,89064,5039102,Oda_do_mlodosci_.html
+odprawa-poslow-greckich http://odprawa_poslow_greckich.lektury.gazeta.pl/lektury/1,93754,5632238,Odprawa_poslow_greckich.html
+piesni-ksiegi-wtore-piesn-v-wieczna-sromota-i-nienagrodzona http://piesn_o_spustoszeniu_podola.lektury.gazeta.pl/lektury/1,90236,5184717,Piesn_o_spustoszeniu_Podola.html
+piesn-swietojanska-o-sobotce http://piesn_swietojanska_o_sobotce.lektury.gazeta.pl/lektury/1,90235,5184500,Piesn_swietojanska_o_Sobotce.html
+grob-agamemnona http://podroz_do_ziemi_swietej.lektury.gazeta.pl/lektury/1,89071,5041538,Podroz_do_Ziemi_Swietej_z_Neapolu.html
+przedwiosnie http://przedwiosnie.lektury.gazeta.pl/lektury/1,89150,5054152,Przedwiosnie_.html
+satyry http://satyry.lektury.gazeta.pl/lektury/1,89474,5102514,Satyry.html
+sonety-krymskie http://sonety_krymskie.lektury.gazeta.pl/lektury/1,89091,5043442,Sonety_krymskie_.html
+swietoszek http://swietoszek.lektury.gazeta.pl/lektury/1,93570,5609125,Swietoszek.html
+testament-moj http://testament_moj.lektury.gazeta.pl/lektury/1,89107,5045974,Testament_moj_.html
+treny http://treny.lektury.gazeta.pl/lektury/1,90265,5186977,Treny.html
+urszula-kochanowska http://urszula_kochanowska.lektury.gazeta.pl/lektury/1,88921,5014764,Urszula_Kochanowska_.html
+w-malinowym-chrusniaku http://w_malinowym_chrusniaku.lektury.gazeta.pl/lektury/1,88922,5014945,W_malinowym_chrusniaku_.html
+wesele http://wesele.lektury.gazeta.pl/lektury/1,93757,5636695,Wesele.html
+zemsta http://zemsta.lektury.gazeta.pl/lektury/1,90268,5187676,Zemsta.html
+starozytnosc http://antyk.lektury.gazeta.pl/lektury/1,94412,5402224,Antyk.html
+sredniowiecze http://sredniowiecze.lektury.gazeta.pl/lektury/1,94433,5402392,Sredniowiecze.html
+renesans http://renesans.lektury.gazeta.pl/lektury/1,91344,5409175,Renesans.html
+barok http://barok.lektury.gazeta.pl/lektury/1,91347,5707242,Barok.html
+oswiecenie http://oswiecenie.lektury.gazeta.pl/lektury/1,91352,5409223,Oswiecenie.html
+romantyzm http://romantyzm.lektury.gazeta.pl/lektury/1,91353,5410249,Romantyzm.html
+pozytywizm http://pozytywizm.lektury.gazeta.pl/lektury/1,91355,5409315,Pozytywizm.html
+1,91356,5409328,Mloda_Polska.html http://www.wolnelektury.pl/katalog/modernizm/
+dwudziestolecie-miedzywojenne http://dwudziestolecie_miedzywojenne.lektury.gazeta.pl/lektury/1,91357,5409357,Dwudziestolecie_miedzywojenne.html
diff --git a/scripts/import_links.py b/scripts/import_links.py
new file mode 100644
index 000000000..ea4380fb8
--- /dev/null
+++ b/scripts/import_links.py
@@ -0,0 +1,36 @@
+import sys
+sys.path.insert(0, '../apps')
+sys.path.insert(0, '../lib')
+sys.path.insert(0, '../wolnelektury')
+sys.path.insert(0, '..')
+
+from django.core.management import setup_environ
+from wolnelektury import settings
+import sys
+
+setup_environ(settings)
+
+from catalogue.models import Book, Tag
+
+
+def import_tags(file_name, attribute):
+ for line in file(file_name):
+ slug, link = line.split()
+ link = link.strip('\n')
+ try:
+ book = Book.objects.get(slug=slug)
+ setattr(book, attribute, link)
+ book.save()
+ print 'Link %s for book %s added!' % (link, book)
+ except Book.DoesNotExist:
+ try:
+ tag = Tag.objects.get(slug=slug)
+ setattr(tag, attribute, link)
+ tag.save()
+ print 'Link %s for tag %s added!' % (link, tag)
+ except Tag.DoesNotExist:
+ print 'Invalid slug %s!' % slug
+
+
+import_tags('gazeta-links', 'gazeta_link')
+
diff --git a/scripts/wiki-links b/scripts/wiki-links
new file mode 100644
index 000000000..c9cd04582
--- /dev/null
+++ b/scripts/wiki-links
@@ -0,0 +1,76 @@
+hans-christian-andersen http://pl.wikipedia.org/wiki/Hans_Christian_Andersen
+adam-asnyk http://pl.wikipedia.org/wiki/Adam_Asnyk
+honore-de-balzac http://pl.wikipedia.org/wiki/Honor%C3%A9_de_Balzac
+george-gordon-byron http://pl.wikipedia.org/wiki/George_Gordon_Byron
+anton-czechow http://pl.wikipedia.org/wiki/Anton_Czechow
+aleksander-fredro http://pl.wikipedia.org/wiki/Aleksander_Fredro
+johann-wolfgang-von-goethe http://pl.wikipedia.org/wiki/Goethe
+bruno-jasienski http://pl.wikipedia.org/wiki/Bruno_Jasie%C5%84ski
+franciszek-karpinski http://pl.wikipedia.org/wiki/Franciszek_Karpi%C5%84ski
+jan-kasprowicz http://pl.wikipedia.org/wiki/Jan_Kasprowicz
+jan-kochanowski http://pl.wikipedia.org/wiki/Jan_Kochanowski
+maria-konopnicka http://pl.wikipedia.org/wiki/Maria_Konopnicka
+ignacy-krasicki http://pl.wikipedia.org/wiki/Ignacy_Krasicki
+zygmunt-krasinski http://pl.wikipedia.org/wiki/Zygmunt_Krasi%C5%84ski
+boleslaw-lesmian http://pl.wikipedia.org/wiki/Boles%C5%82aw_Le%C5%9Bmian
+adam-mickiewicz http://pl.wikipedia.org/wiki/Adam_Mickiewicz
+moliere http://pl.wikipedia.org/wiki/Moliere
+jan-andrzej-morsztyn http://pl.wikipedia.org/wiki/Jan_Andrzej_Morsztyn
+artur-oppman http://pl.wikipedia.org/wiki/Artur_Oppman
+boleslaw-prus http://pl.wikipedia.org/wiki/Boles%C5%82aw_Prus
+wladyslaw-stanislaw-reymont http://pl.wikipedia.org/wiki/Reymont
+mikolaj-sep-szarzynski http://pl.wikipedia.org/wiki/Miko%C5%82aj_S%C4%99p_Szarzy%C5%84ski
+william-shakespeare http://pl.wikipedia.org/wiki/Szekspir
+henryk-sienkiewicz http://pl.wikipedia.org/wiki/Henryk_Sienkiewicz
+juliusz-slowacki http://pl.wikipedia.org/wiki/Juliusz_S%C5%82owacki
+sofokles http://pl.wikipedia.org/wiki/Sofokles
+stanislaw-wyspianski http://pl.wikipedia.org/wiki/Stanis%C5%82aw_Wyspia%C5%84ski
+gabriela-zapolska http://pl.wikipedia.org/wiki/Gabriela_Zapolska
+stefan-zeromski http://pl.wikipedia.org/wiki/Stefan_%C5%BBeromski
+brzydkie-kaczatko http://pl.wikipedia.org/wiki/Brzydkie_kacz%C4%85tko
+krolowa-sniegu http://pl.wikipedia.org/wiki/Kr%C3%B3lowa_%C5%9Bniegu
+bogurodzica http://pl.wikipedia.org/wiki/Bogurodzica_(pie%C5%9B%C5%84)
+ojciec-goriot http://pl.wikipedia.org/wiki/Ojciec_Goriot
+giaur http://pl.wikipedia.org/wiki/Giaur_(George_Byron)
+zemsta http://pl.wikipedia.org/wiki/Zemsta_(komedia)
+cierpienia-mlodego-wertera http://pl.wikipedia.org/wiki/Cierpienia_m%C5%82odego_Wertera
+odprawa-poslow-greckich http://pl.wikipedia.org/wiki/Odprawa_pos%C5%82%C3%B3w_greckich
+dym http://pl.wikipedia.org/wiki/Dym_(nowela)
+nasza-szkapa http://pl.wikipedia.org/wiki/Nasza_szkapa
+satyry-czesc-pierwsza-zona-modna http://pl.wikipedia.org/wiki/%C5%BBona_modna
+nie-boska-komedia http://pl.wikipedia.org/wiki/Nie-Boska_komedia
+dziewczyna http://pl.wikipedia.org/wiki/Dziewczyna_(Boles%C5%82aw_Le%C5%9Bmian)
+dziady http://pl.wikipedia.org/wiki/Dziady_(dramat)
+oda-do-mlodosci http://pl.wikipedia.org/wiki/Oda_do_m%C5%82odo%C5%9Bci
+ballady-i-romanse-romantycznosc http://pl.wikipedia.org/wiki/Romantyczno%C5%9B%C4%87_(Mickiewicz)
+sonety-krymskie http://pl.wikipedia.org/wiki/Sonety_krymskie
+reduta-ordona http://pl.wikipedia.org/wiki/Reduta_Ordona
+swietoszek http://pl.wikipedia.org/wiki/%C5%9Awi%C4%99toszek
+lalka http://pl.wikipedia.org/wiki/Lalka_(powie%C5%9B%C4%87)
+chlopi http://pl.wikipedia.org/wiki/Ch%C5%82opi_(powie%C5%9B%C4%87)
+hamlet http://pl.wikipedia.org/wiki/Hamlet
+romeo-i-julia http://pl.wikipedia.org/wiki/Romeo_i_Julia
+janko-muzykant http://pl.wikipedia.org/wiki/Janko_Muzykant
+latarnik http://pl.wikipedia.org/wiki/Latarnik_(nowela)
+w-pustyni-i-w-puszczy http://pl.wikipedia.org/wiki/W_pustyni_i_w_puszczy
+balladyna http://pl.wikipedia.org/wiki/Balladyna
+grob-agamemnona http://pl.wikipedia.org/wiki/Gr%C3%B3b_Agamemnona
+hymn-o-zachodzie-slonca-na-morzu http://pl.wikipedia.org/wiki/Hymn_o_zachodzie_s%C5%82o%C5%84ca_na_morzu
+odpowiedz-na-psalmy-przyszlosci http://pl.wikipedia.org/wiki/Odpowied%C5%BA_na_Psalmy_przysz%C5%82o%C5%9Bci
+testament-moj http://pl.wikipedia.org/wiki/Testament_m%C3%B3j
+antygona http://pl.wikipedia.org/wiki/Antygona_(dramat)
+krol-edyp http://pl.wikipedia.org/wiki/Kr%C3%B3l_Edyp
+wesele http://pl.wikipedia.org/wiki/Wesele_(dramat)
+moralnosc-pani-dulskiej http://pl.wikipedia.org/wiki/Moralno%C5%9B%C4%87_Pani_Dulskiej
+ludzie-bezdomni http://pl.wikipedia.org/wiki/Ludzie_bezdomni
+przedwiosnie http://pl.wikipedia.org/wiki/Przedwio%C5%9Bnie_(powie%C5%9B%C4%87)
+silaczka http://pl.wikipedia.org/wiki/Si%C5%82aczka
+treny http://pl.wikipedia.org/wiki/Treny
+starozytnosc http://pl.wikipedia.org/wiki/Staro%C5%BCytno%C5%9B%C4%87
+sredniowiecze http://pl.wikipedia.org/wiki/%C5%9Aredniowiecze
+renesans http://pl.wikipedia.org/wiki/Renesans
+barok http://pl.wikipedia.org/wiki/Barok
+oswiecenie http://pl.wikipedia.org/wiki/O%C5%9Bwiecenie_(okres)
+romantyzm http://pl.wikipedia.org/wiki/Romantyzm
+pozytywizm http://pl.wikipedia.org/wiki/Pozytywizm
+dwudziestolecie-miedzywojenne http://pl.wikipedia.org/wiki/Dwudziestolecie_mi%C4%99dzywojenne_w_Polsce
--
2.20.1
From 8d9f0f1b6f7607545370131c60738e37c3fd4d6f Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 22:23:35 +0200
Subject: [PATCH 07/16] Added wiki links to catalogue.Book and catalogue.Tag
models.
---
apps/catalogue/migrations/0005_add_wiki_links.py | 13 +++++++++++++
apps/catalogue/models.py | 3 +++
2 files changed, 16 insertions(+)
create mode 100644 apps/catalogue/migrations/0005_add_wiki_links.py
diff --git a/apps/catalogue/migrations/0005_add_wiki_links.py b/apps/catalogue/migrations/0005_add_wiki_links.py
new file mode 100644
index 000000000..3ac91df21
--- /dev/null
+++ b/apps/catalogue/migrations/0005_add_wiki_links.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from django.db import models
+
+
+class Migration:
+ def forwards(self):
+ db.add_column('catalogue_tag', 'wiki_link', models.CharField(blank=True, max_length=240))
+ db.add_column('catalogue_book', 'wiki_link', models.CharField(blank=True, max_length=240))
+
+ def backwards(self):
+ db.delete_column('catalogue_tag', 'wiki_link')
+ db.delete_column('catalogue_book', 'wiki_link')
diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py
index 5bfe6cbb8..4dde4e0d8 100644
--- a/apps/catalogue/models.py
+++ b/apps/catalogue/models.py
@@ -48,6 +48,7 @@ class Tag(TagBase):
user = models.ForeignKey(User, blank=True, null=True)
book_count = models.IntegerField(_('book count'), default=0, blank=False, null=False)
gazeta_link = models.CharField(blank=True, max_length=240)
+ wiki_link = models.CharField(blank=True, max_length=240)
def has_description(self):
return len(self.description) > 0
@@ -90,6 +91,8 @@ class Book(models.Model):
parent_number = models.IntegerField(_('parent number'), default=0)
extra_info = JSONField(_('extra information'))
gazeta_link = models.CharField(blank=True, max_length=240)
+ wiki_link = models.CharField(blank=True, max_length=240)
+
# Formats
xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
--
2.20.1
From d9e5d0484e0bae0268b4575e189700beda93f93d Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 22:24:45 +0200
Subject: [PATCH 08/16] Added importing links from wikipedia to import_links.py
script.
---
scripts/import_links.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/scripts/import_links.py b/scripts/import_links.py
index ea4380fb8..671212d83 100644
--- a/scripts/import_links.py
+++ b/scripts/import_links.py
@@ -13,7 +13,7 @@ setup_environ(settings)
from catalogue.models import Book, Tag
-def import_tags(file_name, attribute):
+def import_links(file_name, attribute):
for line in file(file_name):
slug, link = line.split()
link = link.strip('\n')
@@ -32,5 +32,5 @@ def import_tags(file_name, attribute):
print 'Invalid slug %s!' % slug
-import_tags('gazeta-links', 'gazeta_link')
-
+import_links('gazeta-links', 'gazeta_link')
+import_links('wiki-links', 'wiki_link')
--
2.20.1
From 6dc2540f663c92368314d610d161f3044fcc845c Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 22:29:08 +0200
Subject: [PATCH 09/16] Displaying links to Wikipedia in templates.
---
wolnelektury/templates/catalogue/book_detail.html | 5 ++++-
wolnelektury/templates/catalogue/tagged_object_list.html | 3 +++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/wolnelektury/templates/catalogue/book_detail.html b/wolnelektury/templates/catalogue/book_detail.html
index a31d8c43f..b9e95d0f7 100644
--- a/wolnelektury/templates/catalogue/book_detail.html
+++ b/wolnelektury/templates/catalogue/book_detail.html
@@ -100,7 +100,10 @@
- Lektura na wiki projektu
- Lektura w CBN Polona
{% if book.gazeta_link %}
- - Lektura w Lektury.Gazeta.pl
+ - Opis lektury w Lektury.Gazeta.pl
+ {% endif %}
+ {% if book.wiki_link %}
+ - Opis lektury w Wikipedii
{% endif %}
diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html
index d1d0b56b6..06e437d5c 100644
--- a/wolnelektury/templates/catalogue/tagged_object_list.html
+++ b/wolnelektury/templates/catalogue/tagged_object_list.html
@@ -33,6 +33,9 @@
{% if last_tag.gazeta_link %}
{{ last_tag }} w Lektury.Gazeta.pl
{% endif %}
+ {% if last_tag.wiki_link %}
+ {{ last_tag }} w Wikipedii
+ {% endif %}
{% for book in object_list %}
-
--
2.20.1
From 831053edbeb1ae2dd5e2a5fa4d2dbdbe0640c96d Mon Sep 17 00:00:00 2001
From: =?utf8?q?Marek=20St=C4=99pniowski?=
Date: Thu, 23 Apr 2009 23:17:44 +0200
Subject: [PATCH 10/16] Added new partners' logos to footer.
---
wolnelektury/media/img/footer.png | Bin 46440 -> 68428 bytes
wolnelektury/templates/base.html | 20 +-------------------
2 files changed, 1 insertion(+), 19 deletions(-)
diff --git a/wolnelektury/media/img/footer.png b/wolnelektury/media/img/footer.png
index 884ce95852b0714ecfd540fe5a84d6bde83888d0..f360e0159f7846319f661bb0217197801eb413f3 100644
GIT binary patch
literal 68428
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
!!
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