This commit is contained in:
Soren I. Bjornstad 2014-06-17 08:14:49 -05:00
commit e0777602db
76 changed files with 819 additions and 521 deletions

View file

@ -1,7 +1,11 @@
Please see the README file for basic requirements. Please see the README file for basic requirements.
You also need to have the python-pyqt development packages installed In addition to the basic requirements, you also need the PyQt development
(specifically, you need the binary pyuic4). tools (specifically pyrcc4 and pyuic4). These are often contained in a
separate package on Linux, such as 'pyqt4-dev-tools' on Debian/Ubuntu. On a Mac
they are part of the PyQt source install.
Windows users, please see the note at the bottom of this file before proceeding.
To use the development version: To use the development version:
@ -9,7 +13,11 @@ $ git clone https://github.com/dae/anki.git
$ cd anki $ cd anki
$ ./tools/build_ui.sh $ ./tools/build_ui.sh
Make sure you rebuild the UI every time you update the sources. If you get any errors, you will not be able to proceed, so please return to
the top and check the requirements again.
ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you
will get errors down the road.
The translations are stored in a bazaar repo for integration with Launchpad's The translations are stored in a bazaar repo for integration with Launchpad's
translation services. If you want to use a language other than English: translation services. If you want to use a language other than English:
@ -31,3 +39,20 @@ Before contributing code, please read the LICENSE file.
If you'd like to contribute translations, please see the translations section If you'd like to contribute translations, please see the translations section
of http://ankisrs.net/docs/manual.html#_contributing of http://ankisrs.net/docs/manual.html#_contributing
WINDOWS USERS:
I have not tested the build scripts on Windows, so you'll need to solve any
problems you encounter on your own. The easiest way is to use a source
tarball instead of git, as that way you don't need to build the UI yourself.
If you do want to use git, a user contributed the following, which should get
you most of the way there:
1) Install "git bash".
2) In the tools directory, modify build_ui.sh. Locate the line that reads
"pyuic4 $i -o $py" and alter it to be of the following form:
"<python-path-string>" "<pyuic-path-string>" $i -o $py
These two paths must point to your python executable, and to pyuic.py, on your
system. Typical paths would be:
<python-path> = C:\\Python27\\python.exe
<pyuic-path-string> = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py

View file

@ -30,6 +30,6 @@ if arch[1] == "ELF":
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % ( sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
sys.version_info[1], arch[0][0:2]))) sys.version_info[1], arch[0][0:2])))
version="2.0.19" # build scripts grep this line, so preserve formatting version="2.0.26" # build scripts grep this line, so preserve formatting
from anki.storage import Collection from anki.storage import Collection
__all__ = ["Collection"] __all__ = ["Collection"]

View file

@ -1,4 +1,142 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIFFjCCA/6gAwIBAgIQRi3682cfg0pV4BcKiOriGTANBgkqhkiG9w0BAQUFADBy
MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEYMBYGA1UE
AxMPRXNzZW50aWFsU1NMIENBMB4XDTE0MDQxMDAwMDAwMFoXDTE3MDQwOTIzNTk1
OVowWzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMR4wHAYDVQQL
ExVFc3NlbnRpYWxTU0wgV2lsZGNhcmQxFjAUBgNVBAMUDSouYW5raXdlYi5uZXQw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+A1hdxZ8NNorbzRF9qutY
7DNuKQdXRjMuRszAeAHI8Ln6+zfvMvLPvCHrZ9aDNTpKhAQBuE7NrFkR9oR0xHT8
JtgVQKJ5zvVFfZmNoHyWYW0+dj8ay1Y/74V/I4Xhb43Fk4Q7UBgAl0kwm5vDWC6U
HsA9/KPfEbWCOLSVoA1sU60viggnfbIl0XmNkkt355KvUdJgT5LisOfi8KvH58RN
X8RHDsLI+Kv3rc11hLxwJ171NC0bPvjOIvQ5NKlNeDFZASx00kaGE3H26r1it2Gr
hrR8IlTSV967JDpYi1FO+Aom54/OZ6ozE/JNsbKlcwmdH2CO2aMizFZfoQmYEsyr
AgMBAAGjggG9MIIBuTAfBgNVHSMEGDAWgBTay+qtWwhdzP/8JlTOSeVVxjj0+DAd
BgNVHQ4EFgQU8P8DDEM/4k6bCQr8Z9BeoGw5ANgwDgYDVR0PAQH/BAQDAgWgMAwG
A1UdEwEB/wQCMAAwNAYDVR0lBC0wKwYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEE
AYI3CgMDBglghkgBhvhCBAEwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQICBzArMCkG
CCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAIBgZngQwB
AgEwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5jb21vZG9jYS5jb20vRXNz
ZW50aWFsU1NMQ0EuY3JsMG4GCCsGAQUFBwEBBGIwYDA4BggrBgEFBQcwAoYsaHR0
cDovL2NydC5jb21vZG9jYS5jb20vRXNzZW50aWFsU1NMQ0FfMi5jcnQwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTAlBgNVHREEHjAcgg0qLmFu
a2l3ZWIubmV0ggthbmtpd2ViLm5ldDANBgkqhkiG9w0BAQUFAAOCAQEAR2JPdym8
MsSqmi2EyB4zR/UZH7XQUdIwx/1NhKf1XTN9akTNsS2y6Vp+TvaRVknEm7Z1i8CU
xiSZicsUOUr8MCzDVtTl3KUuYNUdsv0yXwTvGc01xJ26ix+KTmmQVKBq86gYGzXI
pLG0mfG1UAdZc6MxhcPXDROppvGXWk2vb6xYryVQiqV45SCiX2a4PtIf8zqO62eD
Xqazh3fpJ685rBnvWvpi8teYlYbJpJoaFEHa5ime2VixEYPVnfY2DBbiLQVImCgF
4NLwbCh79uD90CiGXHZbVUCq8fNf2gYzhK08erbDWhK39DzkBcE1XlbsPYzN9fUL
E1Re6AD7oBwALQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFAzCCA+ugAwIBAgIQGLLLuqME8aAPwfLzJkYqSjANBgkqhkiG9w0BAQUFADCB
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
MDBaFw0xOTEyMzEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh
dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9E
TyBDQSBMaW1pdGVkMRgwFgYDVQQDEw9Fc3NlbnRpYWxTU0wgQ0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt8AiwcsargxIxF3CJhakgEtSYau2A1NHf
5I5ZLdOWIY120j8YC0YZYwvHIPPlC92AGvFaoL0dds23Izp0XmEbdaqb1IX04XiR
0y3hr/yYLgbSeT1awB8hLRyuIVPGOqchfr7tZ291HRqfalsGs2rjsQuqag7nbWzD
ypWMN84hHzWQfdvaGlyoiBSyD8gSIF/F03/o4Tjg27z5H6Gq1huQByH6RSRQXScq
oChBRVt9vKCiL6qbfltTxfEFFld+Edc7tNkBdtzffRDPUanlOPJ7FAB1WfnwWdsX
Pvev5gItpHnBXaIcw5rIp6gLSApqLn8tl2X2xQScRMiZln5+pN0vAgMBAAGjggGD
MIIBfzAfBgNVHSMEGDAWgBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAdBgNVHQ4EFgQU
2svqrVsIXcz//CZUzknlVcY49PgwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI
MAYBAf8CAQAwIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMD4GA1Ud
IAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21v
ZG8uY29tL0NQUzBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2Nh
LmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBsBggrBgEFBQcB
AQRgMF4wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NvbW9k
b1VUTlNHQ0NBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2Eu
Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAtlzR6QDLqcJcvgTtLeRJ3rvuq1xqo2l/z
odueTZbLN3qo6u6bldudu+Ennv1F7Q5Slqz0J790qpL0pcRDAB8OtXj5isWMcL2a
ejGjKdBZa0wztSz4iw+SY1dWrCRnilsvKcKxudokxeRiDn55w/65g+onO7wdQ7Vu
F6r7yJiIatnyfKH2cboZT7g440LX8NqxwCPf3dfxp+0Jj1agq8MLy6SSgIGSH6lv
+Wwz3D5XxqfyH8wqfOQsTEZf6/Nh9yvENZ+NWPU6g0QO2JOsTGvMd/QDzczc4BxL
XSXaPV7Od4rhPsbXlM1wSTz/Dr0ISKvlUhQVnQ6cGodWaK2cCQBk
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqzCCA5OgAwIBAgIQLnmDLpCIh+qLjvMabuZ6RDANBgkqhkiG9w0BAQUFADCB
kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw
IFNHQzAeFw0wNjEyMDEwMDAwMDBaFw0yMDA1MzAxMDQ4MzhaMIGBMQswCQYDVQQG
EwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm
b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RP
IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZ
rts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAh
TaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23Iw
ambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVD
iOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ
0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4IBCTCCAQUwHwYDVR0jBBgw
FoAUUzLRs89/+uDxoF2FTpLSnkUdtE8wHQYDVR0OBBYEFAtY5YvGTBU3pECpMKkh
vkc2Wlb/MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdJQQZ
MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATARBgNVHSAECjAIMAYGBFUdIAAwbQYD
VR0fBGYwZDAxoC+gLYYraHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLURBVEFD
b3JwU0dDLmNybDAvoC2gK4YpaHR0cDovL2NybC5jb21vZG8ubmV0L1VUTi1EQVRB
Q29ycFNHQy5jcmwwDQYJKoZIhvcNAQEFBQADggEBANheksSuFNxDrcKkw2dFBx35
N6IZxxw3NZETHAfEfUKmDvCGXENrDkTPviRhOkKpzp1Mr3k5cN0OBCBOlZw83rdg
umNDQO1qD4FJRrsek8BL8/jhNkkbb7YMDfKQV4r8bZPyKMf6hgoosxcOWYoutr/N
4axMZmzyVZFWtzK/seR9teg6ti/bspzaUJOOTsWsmn5cnhI8O03GUHCzZSuO92uh
uyXAALv17BZlgQ771KMhlneaqHS8U6rCOVD/CwIJYcyVt9eIavZcxWjTFJUaR1/Z
+y3kL48ThqsxE0ATrG7ttRAwixtQqc7ujMrrfLW5Fj3U+m+SbR6ivfsCSsVwvvE=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEpjCCA46gAwIBAgIQRurwlgVMxeP6Zepun0LGZDANBgkqhkiG9w0BAQUFADBv
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
gZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMRswGQYDVQQDExJVVE4gLSBEQVRBQ29y
cCBTR0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf7lgQoituVcSO
vy5GCefgCA8uK3oTlBu99raAjmUFkwAevK/iD44ZDRJH7Kyto/oucPjebvtWQhWe
LlzvI94huQV2JxkPT9bDnLS+lBlj8qYRCutTSJy+8ik7FugaoEymyfQYWWjAcPJT
AMBeUIKlVm82+UrgRIagTU7WR25JSstn16bEBbmOHvT8/83nNuCcBWyyMyIV0LTg
zBfAssD0/jI/KSqVe9jyp04PVHyhDYCzCQPB/1zdXpo+vK68R4pqrnHKH7EquF9C
BQvsRjDRcgvK6VZt9e/feL5hurKlrgRMvKisaRWXve/rtIy/NfjUw9EoDlw6n3AY
MyB3xKKvAgMBAAGjggEXMIIBEzAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
JMtUGjAdBgNVHQ4EFgQUUzLRs89/+uDxoF2FTpLSnkUdtE8wDgYDVR0PAQH/BAQD
AgEGMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZI
AYb4QgQBMBEGA1UdIAQKMAgwBgYEVR0gADB7BgNVHR8EdDByMDigNqA0hjJodHRw
Oi8vY3JsLmNvbW9kb2NhLmNvbS9BZGRUcnVzdEV4dGVybmFsQ0FSb290LmNybDA2
oDSgMoYwaHR0cDovL2NybC5jb21vZG8ubmV0L0FkZFRydXN0RXh0ZXJuYWxDQVJv
b3QuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBjhpIQsRP6N76OKrYbikP1XK4OFN/3
aUB/vxpxAAnYv9QkSr/gk/8B2AvGD+x+R5ywXfd8FJ38wDOShFvSg/RS4iJYdPxD
Gz+no1jaA/288Drk7cwSu8m5rnsEoARyv+neLdKnUWYAc9K9fqqeU5Z9abIYPo6t
VlB+99Ww/zliZYKMllfDj/dg9sKNNIf8T0Pl278cqvaGzebfET+NB/dtgxPAOIg5
YKF+MOHjiD6ku2NvLOmKaCzulmmsBGHhT04OnXJM9nk4yMdIaW+UD3S0vMjPV025
dXGWDYoGC+vd0PA8fcYumEZqOMcCtci4smV13tqQCLZ3uFMAJctHynNf
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw
cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV
@ -53,3 +191,59 @@ Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
-----END CERTIFICATE----- -----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEhjCCA26gAwIBAgIQUkIGSk83/kNpSHqWZ/9dJzANBgkqhkiG9w0BAQUFADBv
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
gZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR8wHQYDVQQDExZVVE4tVVNFUkZpcnN0
LUhhcmR3YXJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsffDOD+0
qH/POYJRZ9Btn9L/WPPnnyvsDYlUmbk4mRb34CF5SMK7YXQSlh08anLVPBBnOjnt
KxPNZuuVCTOkbJex6MbswXV5nEZejavQav25KlUXEFSzGfCa9vGxXbanbfvgcRdr
ooj7AN/+GjF3DJoBerEy4ysBBzhuw6VeI7xFm3tQwckwj9vlK3rTW/szQB6g1ZgX
vIuHw4nTXaCOsqqq9o5piAbF+okh8widaS4JM5spDUYPjMxJNLBpUb35Bs1orWZM
vD6sYb0KiA7I3z3ufARMnQpea5HW7sftKI2rTYeJc9BupNAeFosU4XZEA39jrOTN
SZzFkvSrMqFIWwIDAQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8D
veAky1QaMB0GA1UdDgQWBBShcl8mGyiYQ5VdBzfVhZadS9LDRTAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwewYDVR0f
BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQWRkVHJ1c3RFeHRl
cm5hbENBUm9vdC5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BZGRU
cnVzdEV4dGVybmFsQ0FSb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAYGQ5WaJD
ZS79+R/WrjO76FMTxIjuIxpszthkWVNTkOg239T88055L9XmjwzvKkFtcb2beDgj
03BLhgz9EqciYhLYzOBR7y3lzQxFoura7X7s9zKa5wU1Xm7CLGhonf+M8cpVh8Qv
sUAG3IQiXG2zzdGbGgozKGYWDL0zwvYH8eOheZTg+NDQ099Shj+p4ckdPoaEsdtf
7uRJQ8E5fc8vlqd1XX5nZ4TlWSBAvzcivwdDtDDhQ4rNA11tuSnZhKf1YmOEhtY3
vm9nu/9iVzmdDE2yKmE9HZzvmncgoC/uGnKdsJ2/eBMnBwpgEZP1Dy7J72skg/6b
kLRLaIHQwvrgPw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFAzCCA+ugAwIBAgIQTM1KmltFEyGMz5AviytRcTANBgkqhkiG9w0BAQUFADCB
lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
SGFyZHdhcmUwHhcNMDYwOTE4MDAwMDAwWhcNMjAwNTMwMTA0ODM4WjBxMQswCQYD
VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT
YWxmb3JkMRowGAYDVQQKExFDb21vZG8gQ0EgTGltaXRlZDEXMBUGA1UEAxMOUG9z
aXRpdmVTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9T3lY
IpPJKD5SEQAvwKkgitctVR4Q57h/4oYqpOxe6eSSWJZUDfMXukGeFZFV78LuACAY
RYMm3yDMPbOhEzEKIVx5g3mrJBVcVvC0lZih2tIb6ha1y7ewwVP5pEba8C4kuGKe
joteK1qWoOpQ6Yj7KCpNmpxIT4O2h65Pxci12f2+P9GnncYsEw3AAcezcPOPabuw
PBDf6wkAhD9u7/zjLbTHXRHM9/Lx9uLjAH4SDt6NfQDKOj32cuh5JaYIFveriP9W
XgkXwFqCBWI0KyhIMpfQhAysExjbnmbHqhSLEWlN8QnTul2piDdi2L8Dm53X5gV+
wmpSqo0HgOqODvMdAgMBAAGjggFuMIIBajAfBgNVHSMEGDAWgBShcl8mGyiYQ5Vd
BzfVhZadS9LDRTAdBgNVHQ4EFgQUuMoR6QYxedvDlMboGSq8uzUWMaQwDgYDVR0P
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwewYDVR0fBHQwcjA4oDagNIYy
aHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLVVTRVJGaXJzdC1IYXJkd2FyZS5j
cmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9VVE4tVVNFUkZpcnN0LUhh
cmR3YXJlLmNybDCBhgYIKwYBBQUHAQEEejB4MDsGCCsGAQUFBzAChi9odHRwOi8v
Y3J0LmNvbW9kb2NhLmNvbS9VVE5BZGRUcnVzdFNlcnZlckNBLmNydDA5BggrBgEF
BQcwAoYtaHR0cDovL2NydC5jb21vZG8ubmV0L1VUTkFkZFRydXN0U2VydmVyQ0Eu
Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQAdtOf5GEhd7fpawx3jt++GFclsE0kWDTGM
MVzn2odkjq8SFqRaLZIaOz4hZaoXw5V+QBz9FGkGGM2sMexq8RaeiSY9WyGN6Oj5
qz2qPMuZ8oZfiFMVBRflqNKFp05Jfdbdx4/OiL9lBeAUtTF37r0qhujop2ot2mUZ
jGfibfZKhWaDtjJNn0IjF9dFQWp2BNStuY9u3MI+6VHyntjzf/tQKvCL/W8NIjYu
zg5G8t6P2jt9HpOs/PQyKw+rAR+lQI/jJJkfXbKqDLnioeeSDJBLU30fKO5WPa8Y
Z0nf1R7CqJgrTEeDgUwuRMLvyGPui3tbMfYmYb95HLCpTqnJUHvi
-----END CERTIFICATE-----

View file

@ -73,9 +73,8 @@ class _Collection(object):
self.crt = int(time.mktime(d.timetuple())) self.crt = int(time.mktime(d.timetuple()))
self.sched = Scheduler(self) self.sched = Scheduler(self)
if not self.conf.get("newBury", False): if not self.conf.get("newBury", False):
mod = self.db.mod self.conf['newBury'] = True
self.sched.unburyCards() self.setMod()
self.db.mod = mod
def name(self): def name(self):
n = os.path.splitext(os.path.basename(self.path))[0] n = os.path.splitext(os.path.basename(self.path))[0]
@ -148,10 +147,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
def close(self, save=True): def close(self, save=True):
"Disconnect from DB." "Disconnect from DB."
if self.db: if self.db:
if not self.conf.get("newBury", False):
mod = self.db.mod
self.sched.unburyCards()
self.db.mod = mod
if save: if save:
self.save() self.save()
else: else:
@ -515,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
afmt = afmt or template['afmt'] afmt = afmt or template['afmt']
for (type, format) in (("q", qfmt), ("a", afmt)): for (type, format) in (("q", qfmt), ("a", afmt)):
if type == "q": if type == "q":
format = format.replace("{{cloze:", "{{cq:%d:" % ( format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
data[4]+1))
format = format.replace("<%cloze:", "<%%cq:%d:" % ( format = format.replace("<%cloze:", "<%%cq:%d:" % (
data[4]+1)) data[4]+1))
else: else:
format = format.replace("{{cloze:", "{{ca:%d:" % ( format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
data[4]+1))
format = format.replace("<%cloze:", "<%%ca:%d:" % ( format = format.replace("<%cloze:", "<%%ca:%d:" % (
data[4]+1)) data[4]+1))
fields['FrontSide'] = stripSounds(d['q']) fields['FrontSide'] = stripSounds(d['q'])
@ -613,13 +606,19 @@ where c.nid == f.id
if self._undo[0] == 1: if self._undo[0] == 1:
old = self._undo[2] old = self._undo[2]
self.clearUndo() self.clearUndo()
self._undo = [1, _("Review"), old + [copy.copy(card)]] wasLeech = card.note().hasTag("leech") or False
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
def _undoReview(self): def _undoReview(self):
data = self._undo[2] data = self._undo[2]
wasLeech = self._undo[3]
c = data.pop() c = data.pop()
if not data: if not data:
self.clearUndo() self.clearUndo()
# remove leech tag if it didn't have it before
if not wasLeech and c.note().hasTag("leech"):
c.note().delTag("leech")
c.note().flush()
# write old data # write old data
c.flush() c.flush()
# and delete revlog entry # and delete revlog entry
@ -696,6 +695,10 @@ select id from notes where mid not in """ + ids2str(self.models.ids()))
self.remNotes(ids) self.remNotes(ids)
# for each model # for each model
for m in self.models.all(): for m in self.models.all():
for t in m['tmpls']:
if t['did'] == "None":
t['did'] = None
problems.append(_("Fixed AnkiDroid deck override bug."))
if m['type'] == MODEL_STD: if m['type'] == MODEL_STD:
# model with missing req specification # model with missing req specification
if 'req' not in m: if 'req' not in m:
@ -753,6 +756,17 @@ select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
"Fixed %d cards with invalid properties.", cnt) % cnt) "Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odue=0 where id in "+ self.db.execute("update cards set odue=0 where id in "+
ids2str(ids)) ids2str(ids))
# cards with odid set when not in a dyn deck
dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
ids = self.db.list("""
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
if ids:
cnt = len(ids)
problems.append(
ngettext("Fixed %d card with invalid properties.",
"Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odid=0, odue=0 where id in "+
ids2str(ids))
# tags # tags
self.tags.registerNotes() self.tags.registerNotes()
# field cache # field cache

View file

@ -38,6 +38,8 @@ DYN_DUE = 6
DYN_REVADDED = 7 DYN_REVADDED = 7
DYN_DUEPRIORITY = 8 DYN_DUEPRIORITY = 8
DYN_MAX_SIZE = 99999
# model types # model types
MODEL_STD = 0 MODEL_STD = 0
MODEL_CLOZE = 1 MODEL_CLOZE = 1

View file

@ -197,6 +197,12 @@ class DeckManager(object):
deck['collapsed'] = not deck['collapsed'] deck['collapsed'] = not deck['collapsed']
self.save(deck) self.save(deck)
def collapseBrowser(self, did):
deck = self.get(did)
collapsed = deck.get('browserCollapsed', False)
deck['browserCollapsed'] = not collapsed
self.save(deck)
def count(self): def count(self):
return len(self.decks) return len(self.decks)

View file

@ -20,10 +20,12 @@ class Exporter(object):
file.close() file.close()
def escapeText(self, text): def escapeText(self, text):
"Escape newlines, tabs and CSS." "Escape newlines, tabs, CSS and quotechar."
text = text.replace("\n", "<br>") text = text.replace("\n", "<br>")
text = text.replace("\t", " " * 8) text = text.replace("\t", " " * 8)
text = re.sub("(?i)<style>.*?</style>", "", text) text = re.sub("(?i)<style>.*?</style>", "", text)
if "\"" in text:
text = "\"" + text.replace("\"", "\"\"") + "\""
return text return text
def cardIds(self): def cardIds(self):
@ -134,8 +136,14 @@ class AnkiExporter(Exporter):
data) data)
# notes # notes
strnids = ids2str(nids.keys()) strnids = ids2str(nids.keys())
notedata = self.src.db.all("select * from notes where id in "+ notedata = []
strnids) for row in self.src.db.all(
"select * from notes where id in "+strnids):
# remove system tags if not exporting scheduling info
if not self.includeSched:
row = list(row)
row[5] = self.removeSystemTags(row[5])
notedata.append(row)
self.dst.db.executemany( self.dst.db.executemany(
"insert into notes values (?,?,?,?,?,?,?,?,?,?,?)", "insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
notedata) notedata)
@ -207,6 +215,9 @@ class AnkiExporter(Exporter):
# such as update the deck description # such as update the deck description
pass pass
def removeSystemTags(self, tags):
return self.src.tags.remFromStr("marked leech", tags)
# Packaged Anki decks # Packaged Anki decks
###################################################################### ######################################################################

View file

@ -18,7 +18,7 @@ class MnemosyneImporter(NoteImporter):
db = DB(self.file) db = DB(self.file)
ver = db.scalar( ver = db.scalar(
"select value from global_variables where key='version'") "select value from global_variables where key='version'")
assert ver.startswith('Mnemosyne SQL 1') assert ver.startswith('Mnemosyne SQL 1') or ver == "2"
# gather facts into temp objects # gather facts into temp objects
curid = None curid = None
notes = {} notes = {}

View file

@ -140,65 +140,29 @@ class SupermemoXmlImporter(NoteImporter):
#s = re.sub(u'>',u'&gt;',s) #s = re.sub(u'>',u'&gt;',s)
#s = re.sub(u'<',u'&lt;',s) #s = re.sub(u'<',u'&lt;',s)
return unicode(btflsoup(s,convertEntities=btflsoup.HTML_ENTITIES )) return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES))
def _afactor2efactor(self, af):
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
def _unescape(self,s,initilize): # Ranges for A-factors and E-factors
"""Note: This method is not used, BeautifulSoup does better job. af_min = 1.2
""" af_max = 6.9
ef_min = 1.3
ef_max = 3.3
if self._unescape_trtable == None: # Sanity checks for the A-factor
self._unescape_trtable = ( if af < af_min:
('&euro;',u''), ('&#32;',u' '), ('&#33;',u'!'), ('&#34;',u'"'), ('&#35;',u'#'), ('&#36;',u'$'), ('&#37;',u'%'), ('&#38;',u'&'), ('&#39;',u"'"), af = af_min
('&#40;',u'('), ('&#41;',u')'), ('&#42;',u'*'), ('&#43;',u'+'), ('&#44;',u','), ('&#45;',u'-'), ('&#46;',u'.'), ('&#47;',u'/'), ('&#48;',u'0'), elif af > af_max:
('&#49;',u'1'), ('&#50;',u'2'), ('&#51;',u'3'), ('&#52;',u'4'), ('&#53;',u'5'), ('&#54;',u'6'), ('&#55;',u'7'), ('&#56;',u'8'), ('&#57;',u'9'), af = af_max
('&#58;',u':'), ('&#59;',u';'), ('&#60;',u'<'), ('&#61;',u'='), ('&#62;',u'>'), ('&#63;',u'?'), ('&#64;',u'@'), ('&#65;',u'A'), ('&#66;',u'B'),
('&#67;',u'C'), ('&#68;',u'D'), ('&#69;',u'E'), ('&#70;',u'F'), ('&#71;',u'G'), ('&#72;',u'H'), ('&#73;',u'I'), ('&#74;',u'J'), ('&#75;',u'K'),
('&#76;',u'L'), ('&#77;',u'M'), ('&#78;',u'N'), ('&#79;',u'O'), ('&#80;',u'P'), ('&#81;',u'Q'), ('&#82;',u'R'), ('&#83;',u'S'), ('&#84;',u'T'),
('&#85;',u'U'), ('&#86;',u'V'), ('&#87;',u'W'), ('&#88;',u'X'), ('&#89;',u'Y'), ('&#90;',u'Z'), ('&#91;',u'['), ('&#92;',u'\\'), ('&#93;',u']'),
('&#94;',u'^'), ('&#95;',u'_'), ('&#96;',u'`'), ('&#97;',u'a'), ('&#98;',u'b'), ('&#99;',u'c'), ('&#100;',u'd'), ('&#101;',u'e'), ('&#102;',u'f'),
('&#103;',u'g'), ('&#104;',u'h'), ('&#105;',u'i'), ('&#106;',u'j'), ('&#107;',u'k'), ('&#108;',u'l'), ('&#109;',u'm'), ('&#110;',u'n'),
('&#111;',u'o'), ('&#112;',u'p'), ('&#113;',u'q'), ('&#114;',u'r'), ('&#115;',u's'), ('&#116;',u't'), ('&#117;',u'u'), ('&#118;',u'v'),
('&#119;',u'w'), ('&#120;',u'x'), ('&#121;',u'y'), ('&#122;',u'z'), ('&#123;',u'{'), ('&#124;',u'|'), ('&#125;',u'}'), ('&#126;',u'~'),
('&#160;',u' '), ('&#161;',u'¡'), ('&#162;',u'¢'), ('&#163;',u'£'), ('&#164;',u'¤'), ('&#165;',u'¥'), ('&#166;',u'¦'), ('&#167;',u'§'),
('&#168;',u'¨'), ('&#169;',u'©'), ('&#170;',u'ª'), ('&#171;',u'«'), ('&#172;',u'¬'), ('&#173;',u'­'), ('&#174;',u'®'), ('&#175;',u'¯'),
('&#176;',u'°'), ('&#177;',u'±'), ('&#178;',u'²'), ('&#179;',u'³'), ('&#180;',u'´'), ('&#181;',u'µ'), ('&#182;',u''), ('&#183;',u'·'),
('&#184;',u'¸'), ('&#185;',u'¹'), ('&#186;',u'º'), ('&#187;',u'»'), ('&#188;',u'¼'), ('&#189;',u'½'), ('&#190;',u'¾'), ('&#191;',u'¿'),
('&#192;',u'À'), ('&#193;',u'Á'), ('&#194;',u'Â'), ('&#195;',u'Ã'), ('&#196;',u'Ä'), ('&Aring;',u'Å'), ('&#197;',u'Å'), ('&#198;',u'Æ'),
('&#199;',u'Ç'), ('&#200;',u'È'), ('&#201;',u'É'), ('&#202;',u'Ê'), ('&#203;',u'Ë'), ('&#204;',u'Ì'), ('&#205;',u'Í'), ('&#206;',u'Î'),
('&#207;',u'Ï'), ('&#208;',u'Ð'), ('&#209;',u'Ñ'), ('&#210;',u'Ò'), ('&#211;',u'Ó'), ('&#212;',u'Ô'), ('&#213;',u'Õ'), ('&#214;',u'Ö'),
('&#215;',u'×'), ('&#216;',u'Ø'), ('&#217;',u'Ù'), ('&#218;',u'Ú'), ('&#219;',u'Û'), ('&#220;',u'Ü'), ('&#221;',u'Ý'), ('&#222;',u'Þ'),
('&#223;',u'ß'), ('&#224;',u'à'), ('&#225;',u'á'), ('&#226;',u'â'), ('&#227;',u'ã'), ('&#228;',u'ä'), ('&#229;',u'å'), ('&#230;',u'æ'),
('&#231;',u'ç'), ('&#232;',u'è'), ('&#233;',u'é'), ('&#234;',u'ê'), ('&#235;',u'ë'), ('&#236;',u'ì'), ('&iacute;',u'í'), ('&#237;',u'í'),
('&#238;',u'î'), ('&#239;',u'ï'), ('&#240;',u'ð'), ('&#241;',u'ñ'), ('&#242;',u'ò'), ('&#243;',u'ó'), ('&#244;',u'ô'), ('&#245;',u'õ'),
('&#246;',u'ö'), ('&#247;',u'÷'), ('&#248;',u'ø'), ('&#249;',u'ù'), ('&#250;',u'ú'), ('&#251;',u'û'), ('&#252;',u'ü'), ('&#253;',u'ý'),
('&#254;',u'þ'), ('&#255;',u'ÿ'), ('&#256;',u'Ā'), ('&#257;',u'ā'), ('&#258;',u'Ă'), ('&#259;',u'ă'), ('&#260;',u'Ą'), ('&#261;',u'ą'),
('&#262;',u'Ć'), ('&#263;',u'ć'), ('&#264;',u'Ĉ'), ('&#265;',u'ĉ'), ('&#266;',u'Ċ'), ('&#267;',u'ċ'), ('&#268;',u'Č'), ('&#269;',u'č'),
('&#270;',u'Ď'), ('&#271;',u'ď'), ('&#272;',u'Đ'), ('&#273;',u'đ'), ('&#274;',u'Ē'), ('&#275;',u'ē'), ('&#276;',u'Ĕ'), ('&#277;',u'ĕ'),
('&#278;',u'Ė'), ('&#279;',u'ė'), ('&#280;',u'Ę'), ('&#281;',u'ę'), ('&#282;',u'Ě'), ('&#283;',u'ě'), ('&#284;',u'Ĝ'), ('&#285;',u'ĝ'),
('&#286;',u'Ğ'), ('&#287;',u'ğ'), ('&#288;',u'Ġ'), ('&#289;',u'ġ'), ('&#290;',u'Ģ'), ('&#291;',u'ģ'), ('&#292;',u'Ĥ'), ('&#293;',u'ĥ'),
('&#294;',u'Ħ'), ('&#295;',u'ħ'), ('&#296;',u'Ĩ'), ('&#297;',u'ĩ'), ('&#298;',u'Ī'), ('&#299;',u'ī'), ('&#300;',u'Ĭ'), ('&#301;',u'ĭ'),
('&#302;',u'Į'), ('&#303;',u'į'), ('&#304;',u'İ'), ('&#305;',u'ı'), ('&#306;',u'IJ'), ('&#307;',u'ij'), ('&#308;',u'Ĵ'), ('&#309;',u'ĵ'),
('&#310;',u'Ķ'), ('&#311;',u'ķ'), ('&#312;',u'ĸ'), ('&#313;',u'Ĺ'), ('&#314;',u'ĺ'), ('&#315;',u'Ļ'), ('&#316;',u'ļ'), ('&#317;',u'Ľ'),
('&#318;',u'ľ'), ('&#319;',u'Ŀ'), ('&#320;',u'ŀ'), ('&#321;',u'Ł'), ('&#322;',u'ł'), ('&#323;',u'Ń'), ('&#324;',u'ń'), ('&#325;',u'Ņ'),
('&#326;',u'ņ'), ('&#327;',u'Ň'), ('&#328;',u'ň'), ('&#329;',u'ʼn'), ('&#330;',u'Ŋ'), ('&#331;',u'ŋ'), ('&#332;',u'Ō'), ('&#333;',u'ō'),
('&#334;',u'Ŏ'), ('&#335;',u'ŏ'), ('&#336;',u'Ő'), ('&#337;',u'ő'), ('&#338;',u'Œ'), ('&#339;',u'œ'), ('&#340;',u'Ŕ'), ('&#341;',u'ŕ'),
('&#342;',u'Ŗ'), ('&#343;',u'ŗ'), ('&#344;',u'Ř'), ('&#345;',u'ř'), ('&#346;',u'Ś'), ('&#347;',u'ś'), ('&#348;',u'Ŝ'), ('&#349;',u'ŝ'),
('&#350;',u'Ş'), ('&#351;',u'ş'), ('&#352;',u'Š'), ('&#353;',u'š'), ('&#354;',u'Ţ'), ('&#355;',u'ţ'), ('&#356;',u'Ť'), ('&#357;',u'ť'),
('&#358;',u'Ŧ'), ('&#359;',u'ŧ'), ('&#360;',u'Ũ'), ('&#361;',u'ũ'), ('&#362;',u'Ū'), ('&#363;',u'ū'), ('&#364;',u'Ŭ'), ('&#365;',u'ŭ'),
('&#366;',u'Ů'), ('&#367;',u'ů'), ('&#368;',u'Ű'), ('&#369;',u'ű'), ('&#370;',u'Ų'), ('&#371;',u'ų'), ('&#372;',u'Ŵ'), ('&#373;',u'ŵ'),
('&#374;',u'Ŷ'), ('&#375;',u'ŷ'), ('&#376;',u'Ÿ'), ('&#377;',u'Ź'), ('&#378;',u'ź'), ('&#379;',u'Ż'), ('&#380;',u'ż'), ('&#381;',u'Ž'),
('&#382;',u'ž'), ('&#383;',u'ſ'), ('&#340;',u'Ŕ'), ('&#341;',u'ŕ'), ('&#342;',u'Ŗ'), ('&#343;',u'ŗ'), ('&#344;',u'Ř'), ('&#345;',u'ř'),
('&#346;',u'Ś'), ('&#347;',u'ś'), ('&#348;',u'Ŝ'), ('&#349;',u'ŝ'), ('&#350;',u'Ş'), ('&#351;',u'ş'), ('&#352;',u'Š'), ('&#353;',u'š'),
('&#354;',u'Ţ'), ('&#355;',u'ţ'), ('&#356;',u'Ť'), ('&#577;',u'ť'), ('&#358;',u'Ŧ'), ('&#359;',u'ŧ'), ('&#360;',u'Ũ'), ('&#361;',u'ũ'),
('&#362;',u'Ū'), ('&#363;',u'ū'), ('&#364;',u'Ŭ'), ('&#365;',u'ŭ'), ('&#366;',u'Ů'), ('&#367;',u'ů'), ('&#368;',u'Ű'), ('&#369;',u'ű'),
('&#370;',u'Ų'), ('&#371;',u'ų'), ('&#372;',u'Ŵ'), ('&#373;',u'ŵ'), ('&#374;',u'Ŷ'), ('&#375;',u'ŷ'), ('&#376;',u'Ÿ'), ('&#377;',u'Ź'),
('&#378;',u'ź'), ('&#379;',u'Ż'), ('&#380;',u'ż'), ('&#381;',u'Ž'), ('&#382;',u'ž'), ('&#383;',u'ſ'),
)
# Scale af to the range 0..1
af_scaled = (af - af_min) / (af_max - af_min)
# Rescale to the interval ef_min..ef_max
ef = ef_min + af_scaled * (ef_max - ef_min)
#m = re.match() return ef
#s = s.replace(code[0], code[1])
## DEFAULT IMPORTER METHODS ## DEFAULT IMPORTER METHODS
@ -249,7 +213,7 @@ class SupermemoXmlImporter(NoteImporter):
nextDue = tLastrep + (float(item.Interval) * 86400.0) nextDue = tLastrep + (float(item.Interval) * 86400.0)
remDays = int((nextDue - time.time())/86400) remDays = int((nextDue - time.time())/86400)
card.due = self.col.sched.today+remDays card.due = self.col.sched.today+remDays
card.factor = int(float(item.AFactor.replace(',','.'))*1000) card.factor = int(self._afactor2efactor(float(item.AFactor.replace(',','.')))*1000)
note.cards[0] = card note.cards[0] = card
# categories & tags # categories & tags

View file

@ -7,8 +7,13 @@ from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
from anki.hooks import addHook from anki.hooks import addHook
from anki.lang import _ from anki.lang import _
latexCmd = ["latex", "-interaction=nonstopmode"] # if you modify these in an add-on, you must make sure to take tmp.tex as the
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"] # input, and output tmp.png as the output file
latexCmds = [
["latex", "-interaction=nonstopmode", "tmp.tex"],
["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"]
]
build = True # if off, use existing media but don't create new build = True # if off, use existing media but don't create new
regexps = { regexps = {
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE), "standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
@ -89,14 +94,11 @@ package in the LaTeX header instead.""") % bad
oldcwd = os.getcwd() oldcwd = os.getcwd()
png = namedtmp("tmp.png") png = namedtmp("tmp.png")
try: try:
# generate dvi # generate png
os.chdir(tmpdir()) os.chdir(tmpdir())
if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log): for latexCmd in latexCmds:
return _errMsg("latex", texpath) if call(latexCmd, stdout=log, stderr=log):
# and png return _errMsg(latexCmd[0], texpath)
if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"],
stdout=log, stderr=log):
return _errMsg("dvipng", texpath)
# add to media # add to media
shutil.copyfile(png, os.path.join(mdir, fname)) shutil.copyfile(png, os.path.join(mdir, fname))
return return

View file

@ -217,6 +217,7 @@ class MediaManager(object):
files = os.listdir(mdir) files = os.listdir(mdir)
else: else:
files = local files = local
renamedFiles = False
for file in files: for file in files:
if not local: if not local:
if not os.path.isfile(file): if not os.path.isfile(file):
@ -236,14 +237,20 @@ class MediaManager(object):
# delete if we already have the NFC form, otherwise rename # delete if we already have the NFC form, otherwise rename
if os.path.exists(nfcFile): if os.path.exists(nfcFile):
os.unlink(file) os.unlink(file)
renamedFiles = True
else: else:
os.rename(file, nfcFile) os.rename(file, nfcFile)
renamedFiles = True
file = nfcFile file = nfcFile
# compare # compare
if nfcFile not in allRefs: if nfcFile not in allRefs:
unused.append(file) unused.append(file)
else: else:
allRefs.discard(nfcFile) allRefs.discard(nfcFile)
# if we renamed any files to nfc format, we must rerun the check
# to make sure the renamed files are not marked as unused
if renamedFiles:
return self.check(local=local)
nohave = [x for x in allRefs if not x.startswith("_")] nohave = [x for x in allRefs if not x.startswith("_")]
return (nohave, unused, invalid) return (nohave, unused, invalid)
@ -333,7 +340,7 @@ class MediaManager(object):
# Illegal characters # Illegal characters
########################################################################## ##########################################################################
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0]') _illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
def stripIllegal(self, str): def stripIllegal(self, str):
return re.sub(self._illegalCharReg, "", str) return re.sub(self._illegalCharReg, "", str)

View file

@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map),
sflds = splitFields(flds) sflds = splitFields(flds)
map = self.fieldMap(m) map = self.fieldMap(m)
ords = set() ords = set()
matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt']) matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt'])
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt']) matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
for fname in matches: for fname in matches:
if fname not in map: if fname not in map:

View file

@ -954,7 +954,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
def _fillDyn(self, deck): def _fillDyn(self, deck):
search, limit, order = deck['terms'][0] search, limit, order = deck['terms'][0]
orderlimit = self._dynOrder(order, limit) orderlimit = self._dynOrder(order, limit)
search += " -is:suspended -is:buried -deck:filtered" if search.strip():
search = "(%s)" % search
search = "%s -is:suspended -is:buried -deck:filtered" % search
try: try:
ids = self.col.findCards(search, order=orderlimit) ids = self.col.findCards(search, order=orderlimit)
except: except:
@ -997,7 +999,7 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
elif o == DYN_DUE: elif o == DYN_DUE:
t = "c.due" t = "c.due"
elif o == DYN_DUEPRIORITY: elif o == DYN_DUEPRIORITY:
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 10000+due end)" % ( t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (
self.today, self.today) self.today, self.today)
else: else:
# if we don't understand the term, default to due order # if we don't understand the term, default to due order

View file

@ -204,12 +204,12 @@ addHook("unloadProfile", stopMplayer)
########################################################################## ##########################################################################
try: try:
import pyaudio import pyaudio
import wave import wave
PYAU_FORMAT = pyaudio.paInt16 PYAU_FORMAT = pyaudio.paInt16
PYAU_CHANNELS = 1 PYAU_CHANNELS = 1
PYAU_RATE = 44100
PYAU_INPUT_INDEX = None PYAU_INPUT_INDEX = None
except: except:
pass pass
@ -244,12 +244,16 @@ class PyAudioThreadedRecorder(threading.Thread):
except NameError: except NameError:
raise Exception( raise Exception(
"Pyaudio not installed (recording not supported on OSX10.3)") "Pyaudio not installed (recording not supported on OSX10.3)")
rate = int(p.get_default_input_device_info()['defaultSampleRate'])
stream = p.open(format=PYAU_FORMAT, stream = p.open(format=PYAU_FORMAT,
channels=PYAU_CHANNELS, channels=PYAU_CHANNELS,
rate=PYAU_RATE, rate=rate,
input=True, input=True,
input_device_index=PYAU_INPUT_INDEX, input_device_index=PYAU_INPUT_INDEX,
frames_per_buffer=chunk) frames_per_buffer=chunk)
all = [] all = []
while not self.finish: while not self.finish:
try: try:
@ -267,7 +271,7 @@ class PyAudioThreadedRecorder(threading.Thread):
wf = wave.open(processingSrc, 'wb') wf = wave.open(processingSrc, 'wb')
wf.setnchannels(PYAU_CHANNELS) wf.setnchannels(PYAU_CHANNELS)
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT)) wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
wf.setframerate(PYAU_RATE) wf.setframerate(rate)
wf.writeframes(data) wf.writeframes(data)
wf.close() wf.close()

View file

@ -353,7 +353,7 @@ group by day order by day""" % (self._limit(), lim),
tot, period, unit)) tot, period, unit))
if total and tot: if total and tot:
perMin = total / float(tot) perMin = total / float(tot)
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % perMin perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % round(perMin)
self._line( self._line(
i, _("Average answer time"), i, _("Average answer time"),
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin)) _("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin))
@ -705,7 +705,7 @@ select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
sum(case when queue=0 then 1 else 0 end), -- new sum(case when queue=0 then 1 else 0 end), -- new
sum(case when queue=-1 then 1 else 0 end) -- susp sum(case when queue<0 then 1 else 0 end) -- susp
from cards where did in %s""" % self._limit()) from cards where did in %s""" % self._limit())
# Footer # Footer

View file

@ -48,10 +48,11 @@ def addForwardOptionalReverse(col):
mm = col.models mm = col.models
m = addBasicModel(col) m = addBasicModel(col)
m['name'] = _("Basic (optional reversed card)") m['name'] = _("Basic (optional reversed card)")
fm = mm.newField(_("Add Reverse")) av = _("Add Reverse")
fm = mm.newField(av)
mm.addField(m, fm) mm.addField(m, fm)
t = mm.newTemplate(_("Card 2")) t = mm.newTemplate(_("Card 2"))
t['qfmt'] = "{{#Add Reverse}}{{"+_("Back")+"}}{{/Add Reverse}}" t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av)
t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}" t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
return m return m

View file

@ -757,6 +757,7 @@ class MediaSyncer(object):
# if the sanity check failed, force a resync # if the sanity check failed, force a resync
self.col.media.forceResync() self.col.media.forceResync()
return "sanityCheckFailed" return "sanityCheckFailed"
return "success"
def removed(self): def removed(self):
return self.col.media.removed() return self.col.media.removed()

View file

@ -4,7 +4,7 @@ from anki.hooks import runFilter
from anki.template import furigana; furigana.install() from anki.template import furigana; furigana.install()
from anki.template import hint; hint.install() from anki.template import hint; hint.install()
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}" clozeReg = r"(?s)\{\{c%s::(.*?)(::(.*?))?\}\}"
modifiers = {} modifiers = {}
def modifier(symbol): def modifier(symbol):
@ -158,36 +158,41 @@ class Template(object):
return txt return txt
# field modifiers # field modifiers
parts = tag_name.split(':',2) parts = tag_name.split(':')
extra = None extra = None
if len(parts) == 1 or parts[0] == '': if len(parts) == 1 or parts[0] == '':
return '{unknown field %s}' % tag_name return '{unknown field %s}' % tag_name
elif len(parts) == 2: else:
(mod, tag) = parts mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts
elif len(parts) == 3:
(mod, extra, tag) = parts
txt = get_or_attr(context, tag) txt = get_or_attr(context, tag)
#Since 'text:' and other mods can affect html on which Anki relies to
#process clozes, we need to make sure clozes are always
#treated after all the other mods, regardless of how they're specified
#in the template, so that {{cloze:text: == {{text:cloze:
#For type:, we return directly since no other mod than cloze (or other
#pre-defined mods) can be present and those are treated separately
mods.reverse()
mods.sort(key=lambda s: not s=="type")
for mod in mods:
# built-in modifiers # built-in modifiers
if mod == 'text': if mod == 'text':
# strip html # strip html
if txt: txt = stripHTML(txt) if txt else ""
return stripHTML(txt)
return ""
elif mod == 'type': elif mod == 'type':
# type answer field; convert it to [[type:...]] for the gui code # type answer field; convert it to [[type:...]] for the gui code
# to process # to process
return "[[%s]]" % tag_name return "[[%s]]" % tag_name
elif mod == 'cq' or mod == 'ca': elif mod.startswith('cq-') or mod.startswith('ca-'):
# cloze deletion # cloze deletion
if txt and extra: mod, extra = mod.split("-")
return self.clozeText(txt, extra, mod[1]) txt = self.clozeText(txt, extra, mod[1]) if txt and extra else ""
else:
return ""
else: else:
# hook-based field modifier # hook-based field modifier
txt = runFilter('fmod_' + mod, txt or '', extra, context, mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups()
txt = runFilter('fmod_' + mod, txt or '', extra or '', context,
tag, tag_name); tag, tag_name);
if txt is None: if txt is None:
return '{unknown field %s}' % tag_name return '{unknown field %s}' % tag_name

View file

@ -17,6 +17,7 @@ import sys
import locale import locale
from hashlib import sha1 from hashlib import sha1
import platform import platform
import traceback
from anki.lang import _, ngettext from anki.lang import _, ngettext
@ -360,6 +361,8 @@ def invalidFilename(str, dirsep=True):
return "/" return "/"
elif (dirsep or not isWin) and "\\" in str: elif (dirsep or not isWin) and "\\" in str:
return "\\" return "\\"
elif str.strip().startswith("."):
return "."
def platDesc(): def platDesc():
# we may get an interrupted system call, so try this in a loop # we may get an interrupted system call, so try this in a loop
@ -382,3 +385,15 @@ def platDesc():
except: except:
continue continue
return theos return theos
# Debugging
##############################################################################
class TimedLog(object):
def __init__(self):
self._last = time.time()
def log(self, s):
path, num, fn, y = traceback.extract_stack(limit=2)[0]
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
self._last = time.time()

View file

@ -31,8 +31,8 @@ system. It's free and open source.")
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina, Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina,
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail, Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,
Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7, Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Kieran Clancy, LaC, Laurent Steffan, Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Kieran Clancy, LaC, Laurent Steffan,
Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur, Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur,
Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael
Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas

View file

@ -96,10 +96,14 @@ class DataModel(QAbstractTableModel):
return return
elif role == Qt.DisplayRole and section < len(self.activeCols): elif role == Qt.DisplayRole and section < len(self.activeCols):
type = self.columnType(section) type = self.columnType(section)
txt = None
for stype, name in self.browser.columns: for stype, name in self.browser.columns:
if type == stype: if type == stype:
txt = name txt = name
break break
# handle case where extension has set an invalid column type
if not txt:
txt = self.browser.columns[0][1]
return txt return txt
else: else:
return return
@ -500,15 +504,18 @@ class Browser(QMainWindow):
def setupSearch(self): def setupSearch(self):
self.filterTimer = None self.filterTimer = None
self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self))
self.connect(self.form.searchButton, self.connect(self.form.searchButton,
SIGNAL("clicked()"), SIGNAL("clicked()"),
self.onSearch) self.onSearch)
self.connect(self.form.searchEdit.lineEdit(), self.connect(self.form.searchEdit.lineEdit(),
SIGNAL("returnPressed()"), SIGNAL("returnPressed()"),
self.onSearch) self.onSearch)
self.setTabOrder(self.form.searchEdit, self.form.tableView)
self.form.searchEdit.setCompleter(None) self.form.searchEdit.setCompleter(None)
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory']) self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
self.connect(self.form.searchEdit.lineEdit(),
SIGNAL("returnPressed()"),
self.onSearch)
def onSearch(self, reset=True): def onSearch(self, reset=True):
"Careful: if reset is true, the current note is saved." "Careful: if reset is true, the current note is saved."
@ -712,11 +719,9 @@ by clicking on one on the left."""))
def setColumnSizes(self): def setColumnSizes(self):
hh = self.form.tableView.horizontalHeader() hh = self.form.tableView.horizontalHeader()
for i in range(len(self.model.activeCols)): hh.setResizeMode(QHeaderView.Interactive)
if hh.visualIndex(i) == len(self.model.activeCols) - 1: hh.setResizeMode(hh.logicalIndex(len(self.model.activeCols)-1),
hh.setResizeMode(i, QHeaderView.Stretch) QHeaderView.Stretch)
else:
hh.setResizeMode(i, QHeaderView.Interactive)
# this must be set post-resize or it doesn't work # this must be set post-resize or it doesn't work
hh.setCascadingSectionResizes(False) hh.setCascadingSectionResizes(False)
@ -727,9 +732,10 @@ by clicking on one on the left."""))
###################################################################### ######################################################################
class CallbackItem(QTreeWidgetItem): class CallbackItem(QTreeWidgetItem):
def __init__(self, root, name, onclick): def __init__(self, root, name, onclick, oncollapse=None):
QTreeWidgetItem.__init__(self, root, [name]) QTreeWidgetItem.__init__(self, root, [name])
self.onclick = onclick self.onclick = onclick
self.oncollapse = oncollapse
def setupTree(self): def setupTree(self):
self.connect( self.connect(
@ -739,22 +745,31 @@ by clicking on one on the left."""))
p.setColor(QPalette.Base, QColor("#d6dde0")) p.setColor(QPalette.Base, QColor("#d6dde0"))
self.form.tree.setPalette(p) self.form.tree.setPalette(p)
self.buildTree() self.buildTree()
self.connect(
self.form.tree, SIGNAL("itemExpanded(QTreeWidgetItem*)"),
lambda item: self.onTreeCollapse(item))
self.connect(
self.form.tree, SIGNAL("itemCollapsed(QTreeWidgetItem*)"),
lambda item: self.onTreeCollapse(item))
def buildTree(self): def buildTree(self):
self.form.tree.clear() self.form.tree.clear()
root = self.form.tree root = self.form.tree
self._systemTagTree(root) self._systemTagTree(root)
self._favTree(root)
self._decksTree(root) self._decksTree(root)
self._modelTree(root) self._modelTree(root)
self._userTagTree(root) self._userTagTree(root)
self.form.tree.expandAll()
self.form.tree.setItemsExpandable(False)
self.form.tree.setIndentation(15) self.form.tree.setIndentation(15)
def onTreeClick(self, item, col): def onTreeClick(self, item, col):
if getattr(item, 'onclick', None): if getattr(item, 'onclick', None):
item.onclick() item.onclick()
def onTreeCollapse(self, item):
if getattr(item, 'oncollapse', None):
item.oncollapse()
def setFilter(self, *args): def setFilter(self, *args):
if len(args) == 1: if len(args) == 1:
txt = args[0] txt = args[0]
@ -804,6 +819,18 @@ by clicking on one on the left."""))
item.setIcon(0, QIcon(":/icons/" + icon)) item.setIcon(0, QIcon(":/icons/" + icon))
return root return root
def _favTree(self, root):
saved = self.col.conf.get('savedFilters', [])
if not saved:
# Don't add favourites to tree if none saved
return
root = self.CallbackItem(root, _("My Searches"), None)
root.setExpanded(True)
root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
for name, filt in saved.items():
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s))
item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
def _userTagTree(self, root): def _userTagTree(self, root):
for t in sorted(self.col.tags.all()): for t in sorted(self.col.tags.all()):
if t.lower() == "marked" or t.lower() == "leech": if t.lower() == "marked" or t.lower() == "leech":
@ -817,10 +844,13 @@ by clicking on one on the left."""))
def fillGroups(root, grps, head=""): def fillGroups(root, grps, head=""):
for g in grps: for g in grps:
item = self.CallbackItem( item = self.CallbackItem(
root, g[0], lambda g=g: self.setFilter( root, g[0],
"deck", head+g[0])) lambda g=g: self.setFilter("deck", head+g[0]),
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]))
item.setIcon(0, QIcon(":/icons/deck16.png")) item.setIcon(0, QIcon(":/icons/deck16.png"))
newhead = head + g[0]+"::" newhead = head + g[0]+"::"
collapsed = self.mw.col.decks.get(g[1]).get('browserCollapsed', False)
item.setExpanded(not collapsed)
fillGroups(item, g[5], newhead) fillGroups(item, g[5], newhead)
fillGroups(root, grps) fillGroups(root, grps)
@ -847,7 +877,7 @@ by clicking on one on the left."""))
d = QDialog(self) d = QDialog(self)
l = QVBoxLayout() l = QVBoxLayout()
l.setMargin(0) l.setMargin(0)
w = AnkiWebView() w = AnkiWebView(canCopy=True)
l.addWidget(w) l.addWidget(w)
w.stdHtml(info + "<p>" + reps) w.stdHtml(info + "<p>" + reps)
bb = QDialogButtonBox(QDialogButtonBox.Close) bb = QDialogButtonBox(QDialogButtonBox.Close)
@ -982,8 +1012,7 @@ where id in %s""" % ids2str(sf))
c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished) c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished)
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.setMargin(0) vbox.setMargin(0)
self._previewWeb = AnkiWebView() self._previewWeb = AnkiWebView(True)
self._previewWeb.setFocusPolicy(Qt.NoFocus)
vbox.addWidget(self._previewWeb) vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox() bbox = QDialogButtonBox()
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole) self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
@ -1193,7 +1222,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
frm = aqt.forms.reposition.Ui_Dialog() frm = aqt.forms.reposition.Ui_Dialog()
frm.setupUi(d) frm.setupUi(d)
(pmin, pmax) = self.col.db.first( (pmin, pmax) = self.col.db.first(
"select min(due), max(due) from cards where type=0") "select min(due), max(due) from cards where type=0 and odid=0")
pmin = pmin or 0
pmax = pmax or 0
txt = _("Queue top: %d") % pmin txt = _("Queue top: %d") % pmin
txt += "\n" + _("Queue bottom: %d") % pmax txt += "\n" + _("Queue bottom: %d") % pmax
frm.label.setText(txt) frm.label.setText(txt)
@ -1708,3 +1739,87 @@ a { margin-right: 1em; }
self.browser.addTags() self.browser.addTags()
elif l == "deletetag": elif l == "deletetag":
self.browser.deleteTags() self.browser.deleteTags()
# Favourites button
######################################################################
class FavouritesLineEdit(QLineEdit):
buttonClicked = pyqtSignal(bool)
def __init__(self, mw, browser, parent=None):
super(FavouritesLineEdit, self).__init__(parent)
self.mw = mw
self.browser = browser
# add conf if missing
if not self.mw.col.conf.has_key('savedFilters'):
self.mw.col.conf['savedFilters'] = {}
self.button = QToolButton(self)
self.button.setStyleSheet('border: 0px;')
self.button.setCursor(Qt.ArrowCursor)
self.button.clicked.connect(self.buttonClicked.emit)
self.setIcon(':/icons/emblem-favorite-off.png')
# flag to raise save or delete dialog on button click
self.doSave = True
# name of current saved filter (if query matches)
self.name = None
self.buttonClicked.connect(self.onClicked)
self.connect(self, SIGNAL("textChanged(QString)"), self.updateButton)
def resizeEvent(self, event):
buttonSize = self.button.sizeHint()
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
self.button.move(self.rect().right() - frameWidth - buttonSize.width(),
(self.rect().bottom() - buttonSize.height() + 1) / 2)
super(FavouritesLineEdit, self).resizeEvent(event)
def setIcon(self, path):
self.button.setIcon(QIcon(path))
def setText(self, txt):
super(FavouritesLineEdit, self).setText(txt)
self.updateButton()
def updateButton(self, reset=True):
# If search text is a saved query, switch to the delete button.
# Otherwise show save button.
txt = unicode(self.text()).strip()
for key, value in self.mw.col.conf['savedFilters'].items():
if txt == value:
self.doSave = False
self.name = key
self.setIcon(QIcon(":/icons/emblem-favorite.png"))
return
self.doSave = True
self.setIcon(QIcon(":/icons/emblem-favorite-off.png"))
def onClicked(self):
if self.doSave:
self.saveClicked()
else:
self.deleteClicked()
def saveClicked(self):
txt = unicode(self.text()).strip()
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText(_("The current search terms will be added as a new "
"item in the sidebar.\n"
"Search name:"))
dlg.setWindowTitle(_("Save search"))
ok = dlg.exec_()
name = dlg.textValue()
if ok:
self.mw.col.conf['savedFilters'][name] = txt
self.updateButton()
self.browser.setupTree()
def deleteClicked(self):
msg = _('Remove "%s" from your saved searches?') % self.name
ok = QMessageBox.question(self, _('Remove search'),
msg, QMessageBox.Yes, QMessageBox.No)
if ok == QMessageBox.Yes:
self.mw.col.conf['savedFilters'].pop(self.name, None)
self.updateButton()
self.browser.setupTree()

View file

@ -120,9 +120,9 @@ class CardLayout(QDialog):
self.model, joinFields(self.note.fields))) self.model, joinFields(self.note.fields)))
for g in pform.groupBox, pform.groupBox_2: for g in pform.groupBox, pform.groupBox_2:
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
pform.frontWeb = AnkiWebView() pform.frontWeb = AnkiWebView(True)
pform.frontPrevBox.addWidget(pform.frontWeb) pform.frontPrevBox.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView() pform.backWeb = AnkiWebView(True)
pform.backPrevBox.addWidget(pform.backWeb) pform.backPrevBox.addWidget(pform.backWeb)
for wig in pform.frontWeb, pform.backWeb: for wig in pform.frontWeb, pform.backWeb:
wig.page().setLinkDelegationPolicy( wig.page().setLinkDelegationPolicy(

View file

@ -41,7 +41,7 @@ class CustomStudy(QDialog):
def onRadioChange(self, idx): def onRadioChange(self, idx):
f = self.form; sp = f.spin f = self.form; sp = f.spin
smin = 1; smax = 9999; sval = 1 smin = 1; smax = DYN_MAX_SIZE; sval = 1
post = _("cards") post = _("cards")
tit = "" tit = ""
spShow = True spShow = True
@ -127,15 +127,15 @@ class CustomStudy(QDialog):
# and then set various options # and then set various options
if i == RADIO_FORGOT: if i == RADIO_FORGOT:
dyn['delays'] = [1] dyn['delays'] = [1]
dyn['terms'][0] = ['rated:%d:1' % spin, 9999, DYN_RANDOM] dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM]
dyn['resched'] = False dyn['resched'] = False
elif i == RADIO_AHEAD: elif i == RADIO_AHEAD:
dyn['delays'] = None dyn['delays'] = None
dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE] dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE]
dyn['resched'] = True dyn['resched'] = True
elif i == RADIO_PREVIEW: elif i == RADIO_PREVIEW:
dyn['delays'] = None dyn['delays'] = None
dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST] dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST]
dyn['resched'] = False dyn['resched'] = False
elif i == RADIO_CRAM: elif i == RADIO_CRAM:
dyn['delays'] = None dyn['delays'] = None

View file

@ -17,6 +17,7 @@ class DeckBrowser(object):
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb) self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0)
def show(self): def show(self):
clearAudioQueue() clearAudioQueue()
@ -65,6 +66,7 @@ class DeckBrowser(object):
key = unicode(evt.text()) key = unicode(evt.text())
def _selDeck(self, did): def _selDeck(self, did):
self.scrollPos = self.web.page().mainFrame().scrollPosition()
self.mw.col.decks.select(did) self.mw.col.decks.select(did)
self.mw.onOverview() self.mw.onOverview()
@ -152,7 +154,7 @@ body { margin: 1em; -webkit-user-select: none; }
if self.web.key == "deckBrowser": if self.web.key == "deckBrowser":
return self.web.page().mainFrame().scrollPosition() return self.web.page().mainFrame().scrollPosition()
else: else:
return QPoint(0,0) return self.scrollPos
def _renderStats(self): def _renderStats(self):
cards, thetime = self.mw.col.db.first(""" cards, thetime = self.mw.col.db.first("""

View file

@ -21,7 +21,7 @@ import anki.js
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg") pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv") audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a")
_html = """ _html = """
<html><head>%s<style> <html><head>%s<style>
@ -690,7 +690,7 @@ class Editor(object):
def onCloze(self): def onCloze(self):
# check that the model is set up for cloze deletion # check that the model is set up for cloze deletion
if '{{cloze:' not in self.note.model()['tmpls'][0]['qfmt']: if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']):
if self.addMode: if self.addMode:
tooltip(_("Warning, cloze deletions will not work until " tooltip(_("Warning, cloze deletions will not work until "
"you switch the type at the top to Cloze.")) "you switch the type at the top to Cloze."))
@ -988,7 +988,7 @@ to a cloze type first, via Edit>Change Note Type."""))
class EditorWebView(AnkiWebView): class EditorWebView(AnkiWebView):
def __init__(self, parent, editor): def __init__(self, parent, editor):
AnkiWebView.__init__(self) AnkiWebView.__init__(self, canFocus=True)
self.editor = editor self.editor = editor
self.strip = self.editor.mw.pm.profile['stripHTML'] self.strip = self.editor.mw.pm.profile['stripHTML']
@ -1117,13 +1117,18 @@ class EditorWebView(AnkiWebView):
url = mime.urls()[0].toString() url = mime.urls()[0].toString()
# chrome likes to give us the URL twice with a \n # chrome likes to give us the URL twice with a \n
url = url.splitlines()[0] url = url.splitlines()[0]
mime = QMimeData() newmime = QMimeData()
link = self.editor.urlToLink(url) link = self.editor.urlToLink(url)
if link: if link:
mime.setHtml(link) newmime.setHtml(link)
elif mime.hasImage():
# if we couldn't convert the url to a link and there's an
# image on the clipboard (such as copy&paste from
# google images in safari), use that instead
return self._processImage(mime)
else: else:
mime.setText(url) newmime.setText(url)
return mime return newmime
# if the user has used 'copy link location' in the browser, the clipboard # if the user has used 'copy link location' in the browser, the clipboard
# will contain the URL as text, and no URLs or HTML. the URL will already # will contain the URL as text, and no URLs or HTML. the URL will already

View file

@ -75,7 +75,7 @@ into a bug report:""")
pluginText = _("""\ pluginText = _("""\
An error occurred in an add-on.<br> An error occurred in an add-on.<br>
Please post on the add-on forum:<br>%s<br>""") Please post on the add-on forum:<br>%s<br>""")
pluginText %= "https://groups.google.com/forum/#!forum/anki-addons" pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
if "addon" in error: if "addon" in error:
txt = pluginText txt = pluginText
else: else:

View file

@ -17,7 +17,6 @@ import aqt.forms
import aqt.modelchooser import aqt.modelchooser
import aqt.deckchooser import aqt.deckchooser
class ChangeMap(QDialog): class ChangeMap(QDialog):
def __init__(self, mw, model, current): def __init__(self, mw, model, current):
QDialog.__init__(self, mw, Qt.Window) QDialog.__init__(self, mw, Qt.Window)
@ -80,6 +79,9 @@ class ImportDialog(QDialog):
self.updateDelimiterButtonText() self.updateDelimiterButtonText()
self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True)) self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True))
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1)) self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1))
# import button
b = QPushButton(_("Import"))
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
self.exec_() self.exec_()
def setupOptions(self): def setupOptions(self):
@ -88,8 +90,6 @@ class ImportDialog(QDialog):
self.mw, self.frm.modelArea, label=False) self.mw, self.frm.modelArea, label=False)
self.deck = aqt.deckchooser.DeckChooser( self.deck = aqt.deckchooser.DeckChooser(
self.mw, self.frm.deckArea, label=False) self.mw, self.frm.deckArea, label=False)
self.connect(self.frm.importButton, SIGNAL("clicked()"),
self.doImport)
def modelChanged(self): def modelChanged(self):
self.importer.model = self.mw.col.models.current() self.importer.model = self.mw.col.models.current()
@ -140,7 +140,7 @@ you can enter it here. Use \\t to represent tab."""),
txt = _("Fields separated by: %s") % d txt = _("Fields separated by: %s") % d
self.frm.autoDetect.setText(txt) self.frm.autoDetect.setText(txt)
def doImport(self, update=False): def accept(self):
self.importer.mapping = self.mapping self.importer.mapping = self.mapping
if not self.importer.mappingOk(): if not self.importer.mappingOk():
showWarning( showWarning(
@ -250,7 +250,7 @@ you can enter it here. Use \\t to represent tab."""),
QDialog.reject(self) QDialog.reject(self)
def helpRequested(self): def helpRequested(self):
openHelp("FileImport") openHelp("importing")
def showUnicodeWarning(): def showUnicodeWarning():
@ -338,6 +338,8 @@ with a different browser.""")
msg = _("""\ msg = _("""\
Invalid file. Please restore from backup.""") Invalid file. Please restore from backup.""")
showWarning(msg) showWarning(msg)
elif "invalidTempFolder" in err:
showWarning(mw.errorHandler.tempFolderMsg())
elif "readonly" in err: elif "readonly" in err:
showWarning(_("""\ showWarning(_("""\
Unable to import from a read-only file.""")) Unable to import from a read-only file."""))

View file

@ -63,6 +63,8 @@ class AnkiQt(QMainWindow):
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore")) self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
# Load profile in a timer so we can let the window finish init and not # Load profile in a timer so we can let the window finish init and not
# close on profile load error. # close on profile load error.
if isMac and qtmajor >= 5:
self.show()
self.progress.timer(10, self.setupProfile, False) self.progress.timer(10, self.setupProfile, False)
def setupUI(self): def setupUI(self):
@ -264,15 +266,19 @@ To import into a password protected profile, please open the profile before atte
def loadCollection(self): def loadCollection(self):
self.hideSchemaMsg = True self.hideSchemaMsg = True
cpath = self.pm.collectionPath()
try: try:
self.col = Collection(self.pm.collectionPath(), log=True) self.col = Collection(cpath, log=True)
except anki.db.Error: except anki.db.Error:
# move back to profile manager # warn user
showWarning("""\ showWarning("""\
Your collection is corrupt. Please see the manual for \ Your collection is corrupt. Please see the manual for \
how to restore from a backup.""") how to restore from a backup.""")
self.unloadProfile() # move it out of the way so the profile can be used again
raise newpath = cpath+str(intTime())
os.rename(cpath, newpath)
# then close
sys.exit(1)
except Exception, e: except Exception, e:
# the custom exception handler won't catch this if we immediately # the custom exception handler won't catch this if we immediately
# unload, so we have to manually handle it # unload, so we have to manually handle it
@ -377,7 +383,9 @@ the manual for information on how to restore from an automatic backup."))
if cleanup: if cleanup:
cleanup(state) cleanup(state)
self.state = state self.state = state
runHook('beforeStateChange', state, oldState, *args)
getattr(self, "_"+state+"State")(oldState, *args) getattr(self, "_"+state+"State")(oldState, *args)
runHook('afterStateChange', state, oldState, *args)
def _deckBrowserState(self, oldState): def _deckBrowserState(self, oldState):
self.deckBrowser.show() self.deckBrowser.show()
@ -405,10 +413,12 @@ the manual for information on how to restore from an automatic backup."))
def _reviewState(self, oldState): def _reviewState(self, oldState):
self.reviewer.show() self.reviewer.show()
self.web.setCanFocus(True)
def _reviewCleanup(self, newState): def _reviewCleanup(self, newState):
if newState != "resetRequired" and newState != "review": if newState != "resetRequired" and newState != "review":
self.reviewer.cleanup() self.reviewer.cleanup()
self.web.setCanFocus(False)
def noteChanged(self, nid): def noteChanged(self, nid):
"Called when a card or note is edited (but not deleted)." "Called when a card or note is edited (but not deleted)."
@ -504,7 +514,7 @@ title="%s">%s</button>''' % (
self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar = aqt.toolbar.Toolbar(self, tweb)
self.toolbar.draw() self.toolbar.draw()
# main area # main area
self.web = aqt.webview.AnkiWebView() self.web = aqt.webview.AnkiWebView(canFocus=True)
self.web.setObjectName("mainText") self.web.setObjectName("mainText")
self.web.setFocusPolicy(Qt.WheelFocus) self.web.setFocusPolicy(Qt.WheelFocus)
self.web.setMinimumWidth(400) self.web.setMinimumWidth(400)

View file

@ -123,7 +123,7 @@ to their original deck.""")
counts = list(self.mw.col.sched.counts()) counts = list(self.mw.col.sched.counts())
finished = not sum(counts) finished = not sum(counts)
for n in range(len(counts)): for n in range(len(counts)):
if counts[n] == 1000: if counts[n] >= 1000:
counts[n] = "1000+" counts[n] = "1000+"
but = self.mw.button but = self.mw.button
if finished: if finished:

View file

@ -18,6 +18,8 @@ class Preferences(QDialog):
self.prof = self.mw.pm.profile self.prof = self.mw.pm.profile
self.form = aqt.forms.preferences.Ui_Preferences() self.form = aqt.forms.preferences.Ui_Preferences()
self.form.setupUi(self) self.form.setupUi(self)
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"), self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
lambda: openHelp("profileprefs")) lambda: openHelp("profileprefs"))
self.setupCollection() self.setupCollection()

View file

@ -179,7 +179,7 @@ documentation for information on using a flash drive.""")
def _defaultBase(self): def _defaultBase(self):
if isWin: if isWin:
if qtmajor >= 5: if False: #qtmajor >= 5:
loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation) loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation)
else: else:
loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation) loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation)
@ -284,7 +284,15 @@ please see:
def _onLangSelected(self): def _onLangSelected(self):
f = self.langForm f = self.langForm
code = langs[f.lang.currentRow()][1] obj = langs[f.lang.currentRow()]
code = obj[1]
name = obj[0]
en = "Are you sure you wish to display Anki's interface in %s?"
r = QMessageBox.question(
None, "Anki", en%name, QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if r != QMessageBox.Yes:
return self._setDefaultLang()
self.meta['defaultLang'] = code self.meta['defaultLang'] = code
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, cPickle.dumps(self.meta), "_global") self.db.execute(sql, cPickle.dumps(self.meta), "_global")

View file

@ -2,9 +2,13 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# imports are all in this file to make moving to pyside easier in the future # imports are all in this file to make moving to pyside easier in the future
# fixme: make sure not to optimize imports on this file
import sip
import os
import sip, os
from anki.utils import isWin, isMac from anki.utils import isWin, isMac
sip.setapi('QString', 2) sip.setapi('QString', 2)
sip.setapi('QVariant', 2) sip.setapi('QVariant', 2)
sip.setapi('QUrl', 2) sip.setapi('QUrl', 2)
@ -18,14 +22,16 @@ from PyQt4.QtGui import *
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from PyQt4.QtNetwork import QLocalServer, QLocalSocket from PyQt4.QtNetwork import QLocalServer, QLocalSocket
def debug(): def debug():
from PyQt4.QtCore import pyqtRemoveInputHook from PyQt4.QtCore import pyqtRemoveInputHook
from pdb import set_trace from pdb import set_trace
pyqtRemoveInputHook() pyqtRemoveInputHook()
set_trace() set_trace()
if os.environ.get("DEBUG"):
import sys, traceback import sys, traceback
if os.environ.get("DEBUG"):
def info(type, value, tb): def info(type, value, tb):
from PyQt4.QtCore import pyqtRemoveInputHook from PyQt4.QtCore import pyqtRemoveInputHook
for line in traceback.format_exception(type, value, tb): for line in traceback.format_exception(type, value, tb):
@ -43,8 +49,3 @@ if qtmajor <= 4 and qtminor <= 6:
import anki.template.furigana import anki.template.furigana
anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>' anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>'
if isWin or isMac:
# we no longer use this, but want it included in the mac+win builds
# so we don't break add-ons that use it. any new add-ons should use
# the above variables instead
from PyQt4 import pyqtconfig

View file

@ -134,7 +134,8 @@ function _updateQA (q, answerMode, klass) {
typeans.focus(); typeans.focus();
} }
if (answerMode) { if (answerMode) {
window.location = "#answer"; var e = $("#answer");
if (e[0]) { e[0].scrollIntoView(); }
} else { } else {
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@ -433,10 +434,11 @@ Please run Tools>Empty Cards""")
return txt.split("::")[0] return txt.split("::")[0]
return txt return txt
matches = [noHint(txt) for txt in matches] matches = [noHint(txt) for txt in matches]
if len(matches) > 1: uniqMatches = set(matches)
txt = ", ".join(matches) if len(uniqMatches) == 1:
else:
txt = matches[0] txt = matches[0]
else:
txt = ", ".join(matches)
return txt return txt
def tokenizeComparison(self, given, correct): def tokenizeComparison(self, given, correct):

View file

@ -170,6 +170,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
elif "10061" in err or "10013" in err or "10053" in err: elif "10061" in err or "10013" in err or "10053" in err:
return _( return _(
"Antivirus or firewall software is preventing Anki from connecting to the internet.") "Antivirus or firewall software is preventing Anki from connecting to the internet.")
elif "10054" in err or "Broken pipe" in err:
return _("Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder.")
elif "Unable to find the server" in err: elif "Unable to find the server" in err:
return _( return _(
"Server not found. Either your connection is down, or antivirus/firewall " "Server not found. Either your connection is down, or antivirus/firewall "
@ -178,6 +180,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
return _("Proxy authentication required.") return _("Proxy authentication required.")
elif "code: 413" in err: elif "code: 413" in err:
return _("Your collection or a media file is too large to sync.") return _("Your collection or a media file is too large to sync.")
elif "EOF occurred in violation of protocol" in err:
return _("Error establishing a secure connection. This is usually caused by filtering software, or problems with your ISP.")
return err return err
def _getUserPass(self): def _getUserPass(self):

View file

@ -240,7 +240,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
ret = [] ret = []
def accept(): def accept():
# work around an osx crash # work around an osx crash
aqt.mw.app.processEvents() #aqt.mw.app.processEvents()
file = unicode(list(d.selectedFiles())[0]) file = unicode(list(d.selectedFiles())[0])
if dirkey: if dirkey:
dir = os.path.dirname(file) dir = os.path.dirname(file)
@ -394,6 +394,7 @@ def tooltip(msg, period=3000, parent=None):
lab.setWindowFlags(Qt.ToolTip) lab.setWindowFlags(Qt.ToolTip)
p = QPalette() p = QPalette()
p.setColor(QPalette.Window, QColor("#feffc4")) p.setColor(QPalette.Window, QColor("#feffc4"))
p.setColor(QPalette.WindowText, QColor("#000000"))
lab.setPalette(p) lab.setPalette(p)
lab.move( lab.move(
aw.mapToGlobal(QPoint(0, -100 + aw.height()))) aw.mapToGlobal(QPoint(0, -100 + aw.height())))

View file

@ -40,7 +40,8 @@ class AnkiWebPage(QWebPage):
class AnkiWebView(QWebView): class AnkiWebView(QWebView):
def __init__(self): # canFocus implies canCopy
def __init__(self, canFocus=False, canCopy=False):
QWebView.__init__(self) QWebView.__init__(self)
self.setRenderHints( self.setRenderHints(
QPainter.TextAntialiasing | QPainter.TextAntialiasing |
@ -59,6 +60,8 @@ class AnkiWebView(QWebView):
self.allowDrops = False self.allowDrops = False
# reset each time new html is set; used to detect if still in same state # reset each time new html is set; used to detect if still in same state
self.key = None self.key = None
self.setCanFocus(canFocus)
self._canCopy = canCopy or canFocus
def keyPressEvent(self, evt): def keyPressEvent(self, evt):
if evt.matches(QKeySequence.Copy): if evt.matches(QKeySequence.Copy):
@ -78,9 +81,7 @@ class AnkiWebView(QWebView):
QWebView.keyReleaseEvent(self, evt) QWebView.keyReleaseEvent(self, evt)
def contextMenuEvent(self, evt): def contextMenuEvent(self, evt):
# lazy: only run in reviewer if not self._canCopy:
import aqt
if aqt.mw.state != "review":
return return
m = QMenu(self) m = QMenu(self)
a = m.addAction(_("Copy")) a = m.addAction(_("Copy"))
@ -131,6 +132,13 @@ button {
def setBridge(self, bridge): def setBridge(self, bridge):
self._bridge.setBridge(bridge) self._bridge.setBridge(bridge)
def setCanFocus(self, canFocus=False):
self._canFocus = canFocus
if self._canFocus:
self.setFocusPolicy(Qt.WheelFocus)
else:
self.setFocusPolicy(Qt.NoFocus)
def eval(self, js): def eval(self, js):
self.page().mainFrame().evaluateJavaScript(js) self.page().mainFrame().evaluateJavaScript(js)

View file

@ -46,7 +46,7 @@
<number>1</number> <number>1</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>9999</number> <number>99999</number>
</property> </property>
</widget> </widget>
</item> </item>

View file

@ -35,6 +35,8 @@
<file>icons/editclear.png</file> <file>icons/editclear.png</file>
<file>icons/view-statistics.png</file> <file>icons/view-statistics.png</file>
<file>icons/emblem-favorite.png</file> <file>icons/emblem-favorite.png</file>
<file>icons/emblem-favorite-dark.png</file>
<file>icons/emblem-favorite-off.png</file>
<file>icons/view-pim-calendar.png</file> <file>icons/view-pim-calendar.png</file>
<file>icons/anki-tag.png</file> <file>icons/anki-tag.png</file>
<file>icons/edit-redo.png</file> <file>icons/edit-redo.png</file>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 457 B

View file

@ -94,20 +94,6 @@
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item> <item>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="importButton">
<property name="text">
<string>&amp;Import</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QScrollArea" name="mappingArea"> <widget class="QScrollArea" name="mappingArea">
<property name="sizePolicy"> <property name="sizePolicy">
@ -133,8 +119,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>402</width> <width>529</width>
<height>206</height> <height>251</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -158,7 +144,6 @@
</layout> </layout>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>importButton</tabstop>
<tabstop>buttonBox</tabstop> <tabstop>buttonBox</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>

View file

@ -9,8 +9,27 @@ def assertException(exception, func):
found = True found = True
assert found assert found
def getEmptyDeck(**kwargs):
# Creating new decks is expensive. Just do it once, and then spin off
# copies from the master.
def getEmptyCol():
if len(getEmptyCol.master) == 0:
(fd, nam) = tempfile.mkstemp(suffix=".anki2") (fd, nam) = tempfile.mkstemp(suffix=".anki2")
os.close(fd)
os.unlink(nam)
col = aopen(nam)
col.db.close()
getEmptyCol.master = nam
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
shutil.copy(getEmptyCol.master, nam)
return aopen(nam)
getEmptyCol.master = ""
# Fallback for when the DB needs options passed in.
def getEmptyDeckWith(**kwargs):
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
os.close(fd)
os.unlink(nam) os.unlink(nam)
return aopen(nam, **kwargs) return aopen(nam, **kwargs)

View file

@ -1,9 +1,9 @@
# coding: utf-8 # coding: utf-8
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
def test_previewCards(): def test_previewCards():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -23,7 +23,7 @@ def test_previewCards():
assert deck.cardCount() == 1 assert deck.cardCount() == 1
def test_delete(): def test_delete():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -39,7 +39,7 @@ def test_delete():
assert deck.db.scalar("select count() from graves") == 2 assert deck.db.scalar("select count() from graves") == 2
def test_misc(): def test_misc():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -49,7 +49,7 @@ def test_misc():
assert c.template()['ord'] == 0 assert c.template()['ord'] == 0
def test_genrem(): def test_genrem():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'' f['Back'] = u''
@ -76,7 +76,7 @@ def test_genrem():
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_gendeck(): def test_gendeck():
d = getEmptyDeck() d = getEmptyCol()
cloze = d.models.byName("Cloze") cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze) d.models.setCurrent(cloze)
f = d.newNote() f = d.newNote()

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
import os import os, tempfile
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyCol
from anki.stdmodels import addBasicModel from anki.stdmodels import addBasicModel
from anki import Collection as aopen from anki import Collection as aopen
@ -11,8 +11,9 @@ newMod = None
def test_create(): def test_create():
global newPath, newMod global newPath, newMod
path = "/tmp/test_attachNew.anki2" (fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
try: try:
os.close(fd)
os.unlink(path) os.unlink(path)
except OSError: except OSError:
pass pass
@ -40,7 +41,7 @@ def test_openReadOnly():
os.unlink(newPath) os.unlink(newPath)
def test_noteAddDelete(): def test_noteAddDelete():
deck = getEmptyDeck() deck = getEmptyCol()
# add a note # add a note
f = deck.newNote() f = deck.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
@ -79,7 +80,7 @@ def test_noteAddDelete():
assert f2.dupeOrEmpty() assert f2.dupeOrEmpty()
def test_fieldChecksum(): def test_fieldChecksum():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"new"; f['Back'] = u"new2" f['Front'] = u"new"; f['Back'] = u"new2"
deck.addNote(f) deck.addNote(f)
@ -92,7 +93,7 @@ def test_fieldChecksum():
"select csum from notes") == int("302811ae", 16) "select csum from notes") == int("302811ae", 16)
def test_addDelTags(): def test_addDelTags():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"1" f['Front'] = u"1"
deck.addNote(f) deck.addNote(f)
@ -111,14 +112,14 @@ def test_addDelTags():
assert len(f.tags) == 2 assert len(f.tags) == 2
def test_timestamps(): def test_timestamps():
deck = getEmptyDeck() deck = getEmptyCol()
assert len(deck.models.models) == 4 assert len(deck.models.models) == 4
for i in range(100): for i in range(100):
addBasicModel(deck) addBasicModel(deck)
assert len(deck.models.models) == 104 assert len(deck.models.models) == 104
def test_furigana(): def test_furigana():
deck = getEmptyDeck() deck = getEmptyCol()
mm = deck.models mm = deck.models
m = mm.current() m = mm.current()
# filter should work # filter should work

View file

@ -1,9 +1,9 @@
# coding: utf-8 # coding: utf-8
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyCol
def test_basic(): def test_basic():
deck = getEmptyDeck() deck = getEmptyCol()
# we start with a standard deck # we start with a standard deck
assert len(deck.decks.decks) == 1 assert len(deck.decks.decks) == 1
# it should have an id of 1 # it should have an id of 1
@ -42,7 +42,7 @@ def test_basic():
deck.sched.deckDueList() deck.sched.deckDueList()
def test_remove(): def test_remove():
deck = getEmptyDeck() deck = getEmptyCol()
# create a new deck, and add a note/card to it # create a new deck, and add a note/card to it
g1 = deck.decks.id("g1") g1 = deck.decks.id("g1")
f = deck.newNote() f = deck.newNote()
@ -68,7 +68,7 @@ def test_remove():
assert deck.noteCount() == 0 assert deck.noteCount() == 0
def test_rename(): def test_rename():
d = getEmptyDeck() d = getEmptyCol()
id = d.decks.id("hello::world") id = d.decks.id("hello::world")
# should be able to rename into a completely different branch, creating # should be able to rename into a completely different branch, creating
# parents as necessary # parents as necessary
@ -89,7 +89,7 @@ def test_rename():
assert n in d.decks.allNames() assert n in d.decks.allNames()
def test_renameForDragAndDrop(): def test_renameForDragAndDrop():
d = getEmptyDeck() d = getEmptyCol()
def deckNames(): def deckNames():
return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ] return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ]

View file

@ -4,7 +4,7 @@ import nose, os, tempfile
from anki import Collection as aopen from anki import Collection as aopen
from anki.exporting import * from anki.exporting import *
from anki.importing import Anki2Importer from anki.importing import Anki2Importer
from shared import getEmptyDeck from shared import getEmptyCol
deck = None deck = None
ds = None ds = None
@ -12,7 +12,7 @@ testDir = os.path.dirname(__file__)
def setup1(): def setup1():
global deck global deck
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"] f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
deck.addNote(f) deck.addNote(f)
@ -36,7 +36,9 @@ def test_export_anki():
deck.decks.setConf(dobj, confId) deck.decks.setConf(dobj, confId)
# export # export
e = AnkiExporter(deck) e = AnkiExporter(deck)
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1]) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
# exporting should not have changed conf for original deck # exporting should not have changed conf for original deck
@ -54,7 +56,9 @@ def test_export_anki():
# conf should be 1 # conf should be 1
assert dobj['conf'] == 1 assert dobj['conf'] == 1
# try again, limited to a deck # try again, limited to a deck
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1]) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname) os.unlink(newname)
e.did = 1 e.did = 1
e.exportInto(newname) e.exportInto(newname)
@ -69,13 +73,15 @@ def test_export_ankipkg():
n['Front'] = u'[sound:今日.mp3]' n['Front'] = u'[sound:今日.mp3]'
deck.addNote(n) deck.addNote(n)
e = AnkiPackageExporter(deck) e = AnkiPackageExporter(deck)
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".apkg")[1]) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
newname = unicode(newname)
os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@nose.with_setup(setup1) @nose.with_setup(setup1)
def test_export_anki_due(): def test_export_anki_due():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"foo" f['Front'] = u"foo"
deck.addNote(f) deck.addNote(f)
@ -92,11 +98,13 @@ def test_export_anki_due():
# export # export
e = AnkiExporter(deck) e = AnkiExporter(deck)
e.includeSched = True e.includeSched = True
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1]) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
# importing into a new deck, the due date should be equivalent # importing into a new deck, the due date should be equivalent
deck2 = getEmptyDeck() deck2 = getEmptyCol()
imp = Anki2Importer(deck2, newname) imp = Anki2Importer(deck2, newname)
imp.run() imp.run()
c = deck2.getCard(c.id) c = deck2.getCard(c.id)
@ -115,7 +123,9 @@ def test_export_anki_due():
@nose.with_setup(setup1) @nose.with_setup(setup1)
def test_export_textnote(): def test_export_textnote():
e = TextNoteExporter(deck) e = TextNoteExporter(deck)
f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) fd, f = tempfile.mkstemp(prefix="ankitest")
f = unicode(f)
os.close(fd)
os.unlink(f) os.unlink(f)
e.exportInto(f) e.exportInto(f)
e.includeTags = True e.includeTags = True

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
from anki.find import Finder from anki.find import Finder
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
def test_parse(): def test_parse():
f = Finder(None) f = Finder(None)
@ -20,7 +20,7 @@ def test_parse():
assert f._tokenize("deck:'two words'") == ["deck:two words"] assert f._tokenize("deck:'two words'") == ["deck:two words"]
def test_findCards(): def test_findCards():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'dog' f['Front'] = u'dog'
f['Back'] = u'cat' f['Back'] = u'cat'
@ -216,7 +216,7 @@ def test_findCards():
assert len(deck.findCards("added:2")) == deck.cardCount() assert len(deck.findCards("added:2")) == deck.cardCount()
def test_findReplace(): def test_findReplace():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'foo' f['Front'] = u'foo'
f['Back'] = u'bar' f['Back'] = u'bar'
@ -243,7 +243,7 @@ def test_findReplace():
f.load(); assert f['Back'] == "reg" f.load(); assert f['Back'] == "reg"
def test_findDupes(): def test_findDupes():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'foo' f['Front'] = u'foo'
f['Back'] = u'bar' f['Back'] = u'bar'

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getUpgradeDeckPath, getEmptyDeck from tests.shared import getUpgradeDeckPath, getEmptyCol
from anki.upgrade import Upgrader from anki.upgrade import Upgrader
from anki.utils import ids2str from anki.utils import ids2str
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \ from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
@ -27,7 +27,7 @@ def test_anki2():
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo") open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
src.close() src.close()
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
# import src into dst # import src into dst
imp = Anki2Importer(dst, srcpath) imp = Anki2Importer(dst, srcpath)
imp.run() imp.run()
@ -50,7 +50,7 @@ def test_anki2():
assert len(os.listdir(dst.media.dir())) == 1 assert len(os.listdir(dst.media.dir())) == 1
def test_anki2_mediadupes(): def test_anki2_mediadupes():
tmp = getEmptyDeck() tmp = getEmptyCol()
# add a note that references a sound # add a note that references a sound
n = tmp.newNote() n = tmp.newNote()
n['Front'] = "[sound:foo.mp3]" n['Front'] = "[sound:foo.mp3]"
@ -60,7 +60,7 @@ def test_anki2_mediadupes():
open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo") open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo")
tmp.close() tmp.close()
# it should be imported correctly into an empty deck # it should be imported correctly into an empty deck
empty = getEmptyDeck() empty = getEmptyCol()
imp = Anki2Importer(empty, tmp.path) imp = Anki2Importer(empty, tmp.path)
imp.run() imp.run()
assert os.listdir(empty.media.dir()) == ["foo.mp3"] assert os.listdir(empty.media.dir()) == ["foo.mp3"]
@ -95,7 +95,7 @@ def test_anki2_mediadupes():
assert "_" in n.fields[0] assert "_" in n.fields[0]
def test_apkg(): def test_apkg():
tmp = getEmptyDeck() tmp = getEmptyCol()
apkg = unicode(os.path.join(testDir, "support/media.apkg")) apkg = unicode(os.path.join(testDir, "support/media.apkg"))
imp = AnkiPackageImporter(tmp, apkg) imp = AnkiPackageImporter(tmp, apkg)
assert os.listdir(tmp.media.dir()) == [] assert os.listdir(tmp.media.dir()) == []
@ -122,7 +122,7 @@ def test_anki1():
os.mkdir(mdir) os.mkdir(mdir)
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo") open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
# import src into dst # import src into dst
imp = Anki1Importer(dst, tmp) imp = Anki1Importer(dst, tmp)
imp.run() imp.run()
@ -138,7 +138,7 @@ def test_anki1():
def test_anki1_diffmodels(): def test_anki1_diffmodels():
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
# import the 1 card version of the model # import the 1 card version of the model
tmp = getUpgradeDeckPath("diffmodels1.anki") tmp = getUpgradeDeckPath("diffmodels1.anki")
imp = Anki1Importer(dst, tmp) imp = Anki1Importer(dst, tmp)
@ -165,7 +165,7 @@ def test_anki1_diffmodels():
def test_suspended(): def test_suspended():
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
# import the 1 card version of the model # import the 1 card version of the model
tmp = getUpgradeDeckPath("suspended12.anki") tmp = getUpgradeDeckPath("suspended12.anki")
imp = Anki1Importer(dst, tmp) imp = Anki1Importer(dst, tmp)
@ -174,7 +174,7 @@ def test_suspended():
def test_anki2_diffmodels(): def test_anki2_diffmodels():
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
# import the 1 card version of the model # import the 1 card version of the model
tmp = getUpgradeDeckPath("diffmodels2-1.apkg") tmp = getUpgradeDeckPath("diffmodels2-1.apkg")
imp = AnkiPackageImporter(dst, tmp) imp = AnkiPackageImporter(dst, tmp)
@ -206,7 +206,7 @@ def test_anki2_diffmodels():
def test_anki2_updates(): def test_anki2_updates():
# create a new empty deck # create a new empty deck
dst = getEmptyDeck() dst = getEmptyCol()
tmp = getUpgradeDeckPath("update1.apkg") tmp = getUpgradeDeckPath("update1.apkg")
imp = AnkiPackageImporter(dst, tmp) imp = AnkiPackageImporter(dst, tmp)
imp.run() imp.run()
@ -232,7 +232,7 @@ def test_anki2_updates():
assert dst.db.scalar("select flds from notes").startswith("goodbye") assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_csv(): def test_csv():
deck = getEmptyDeck() deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/text-2fields.txt")) file = unicode(os.path.join(testDir, "support/text-2fields.txt"))
i = TextImporter(deck, file) i = TextImporter(deck, file)
i.initMapping() i.initMapping()
@ -266,7 +266,7 @@ def test_csv():
deck.close() deck.close()
def test_csv2(): def test_csv2():
deck = getEmptyDeck() deck = getEmptyCol()
mm = deck.models mm = deck.models
m = mm.current() m = mm.current()
f = mm.newField("Three") f = mm.newField("Three")
@ -289,7 +289,7 @@ def test_csv2():
deck.close() deck.close()
def test_supermemo_xml_01_unicode(): def test_supermemo_xml_01_unicode():
deck = getEmptyDeck() deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/supermemo1.xml")) file = unicode(os.path.join(testDir, "support/supermemo1.xml"))
i = SupermemoXmlImporter(deck, file) i = SupermemoXmlImporter(deck, file)
#i.META.logToStdOutput = True #i.META.logToStdOutput = True
@ -297,12 +297,13 @@ def test_supermemo_xml_01_unicode():
assert i.total == 1 assert i.total == 1
cid = deck.db.scalar("select id from cards") cid = deck.db.scalar("select id from cards")
c = deck.getCard(cid) c = deck.getCard(cid)
assert c.factor == 5701 # Applies A Factor-to-E Factor conversion
assert c.factor == 2879
assert c.reps == 7 assert c.reps == 7
deck.close() deck.close()
def test_mnemo(): def test_mnemo():
deck = getEmptyDeck() deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/mnemo.db")) file = unicode(os.path.join(testDir, "support/mnemo.db"))
i = MnemosyneImporter(deck, file) i = MnemosyneImporter(deck, file)
i.run() i.run()

View file

@ -1,14 +1,14 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
from anki.utils import stripHTML from anki.utils import stripHTML
def test_latex(): def test_latex():
d = getEmptyDeck() d = getEmptyCol()
# change latex cmd to simulate broken build # change latex cmd to simulate broken build
import anki.latex import anki.latex
anki.latex.latexCmd[0] = "nolatex" anki.latex.latexCmds[0][0] = "nolatex"
# add a note with latex # add a note with latex
f = d.newNote() f = d.newNote()
f['Front'] = u"[latex]hello[/latex]" f['Front'] = u"[latex]hello[/latex]"
@ -17,7 +17,7 @@ def test_latex():
assert len(os.listdir(d.media.dir())) == 0 assert len(os.listdir(d.media.dir())) == 0
# check the error message # check the error message
msg = f.cards()[0].q() msg = f.cards()[0].q()
assert "executing latex" in msg assert "executing nolatex" in msg
assert "installed" in msg assert "installed" in msg
# check if we have latex installed, and abort test if we don't # check if we have latex installed, and abort test if we don't
for cmd in ("latex", "dvipng"): for cmd in ("latex", "dvipng"):
@ -26,7 +26,7 @@ def test_latex():
print "aborting test; %s is not installed" % cmd print "aborting test; %s is not installed" % cmd
return return
# fix path # fix path
anki.latex.latexCmd[0] = "latex" anki.latex.latexCmds[0][0] = "latex"
# check media db should cause latex to be generated # check media db should cause latex to be generated
d.media.check() d.media.check()
assert len(os.listdir(d.media.dir())) == 1 assert len(os.listdir(d.media.dir())) == 1

View file

@ -4,12 +4,12 @@ import tempfile
import os import os
import time import time
from shared import getEmptyDeck, testDir from shared import getEmptyCol, testDir
# copying files to media folder # copying files to media folder
def test_add(): def test_add():
d = getEmptyDeck() d = getEmptyCol()
dir = tempfile.mkdtemp(prefix="anki") dir = tempfile.mkdtemp(prefix="anki")
path = os.path.join(dir, u"foo.jpg") path = os.path.join(dir, u"foo.jpg")
open(path, "w").write("hello") open(path, "w").write("hello")
@ -22,7 +22,7 @@ def test_add():
assert d.media.addFile(path) == "foo (1).jpg" assert d.media.addFile(path) == "foo (1).jpg"
def test_strings(): def test_strings():
d = getEmptyDeck() d = getEmptyCol()
mf = d.media.filesInStr mf = d.media.filesInStr
mid = d.models.models.keys()[0] mid = d.models.models.keys()[0]
assert mf(mid, "aoeu") == [] assert mf(mid, "aoeu") == []
@ -46,7 +46,7 @@ def test_strings():
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">' assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
def test_deckIntegration(): def test_deckIntegration():
d = getEmptyDeck() d = getEmptyCol()
# create a media dir # create a media dir
d.media.dir() d.media.dir()
# put a file into it # put a file into it
@ -68,7 +68,7 @@ def test_deckIntegration():
assert ret[1] == ["foo.jpg"] assert ret[1] == ["foo.jpg"]
def test_changes(): def test_changes():
d = getEmptyDeck() d = getEmptyCol()
assert d.media._changed() assert d.media._changed()
def added(): def added():
return d.media.db.execute("select fname from log where type = 0") return d.media.db.execute("select fname from log where type = 0")
@ -103,7 +103,7 @@ def test_changes():
assert len(list(d.media.removed())) == 1 assert len(list(d.media.removed())) == 1
def test_illegal(): def test_illegal():
d = getEmptyDeck() d = getEmptyCol()
aString = u"a:b|cd\\e/f\0g*h" aString = u"a:b|cd\\e/f\0g*h"
good = u"abcdefgh" good = u"abcdefgh"
assert d.media.stripIllegal(aString) == good assert d.media.stripIllegal(aString) == good

View file

@ -1,10 +1,10 @@
# coding: utf-8 # coding: utf-8
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
from anki.utils import stripHTML, joinFields from anki.utils import stripHTML, joinFields
def test_modelDelete(): def test_modelDelete():
deck = getEmptyDeck() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -14,7 +14,7 @@ def test_modelDelete():
assert deck.cardCount() == 0 assert deck.cardCount() == 0
def test_modelCopy(): def test_modelCopy():
deck = getEmptyDeck() deck = getEmptyCol()
m = deck.models.current() m = deck.models.current()
m2 = deck.models.copy(m) m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy" assert m2['name'] == "Basic copy"
@ -27,7 +27,7 @@ def test_modelCopy():
assert deck.models.scmhash(m) == deck.models.scmhash(m2) assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields(): def test_fields():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -74,7 +74,7 @@ def test_fields():
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
def test_templates(): def test_templates():
d = getEmptyDeck() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current(); mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t['qfmt'] = "{{Back}}"
@ -107,8 +107,31 @@ def test_templates():
mm.addTemplate(m, t) mm.addTemplate(m, t)
assert not d.models.remTemplate(m, m['tmpls'][0]) assert not d.models.remTemplate(m, m['tmpls'][0])
def test_cloze_ordinals():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
f = d.newNote()
f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
# first card should have first ord
assert c.ord == 0
assert c2.ord == 1
def test_text(): def test_text():
d = getEmptyDeck() d = getEmptyCol()
m = d.models.current() m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}" m['tmpls'][0]['qfmt'] = "{{text:Front}}"
d.models.save(m) d.models.save(m)
@ -118,7 +141,7 @@ def test_text():
assert "helloworld" in f.cards()[0].q() assert "helloworld" in f.cards()[0].q()
def test_cloze(): def test_cloze():
d = getEmptyDeck() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote() f = d.newNote()
assert f.model()['name'] == "Cloze" assert f.model()['name'] == "Cloze"
@ -163,8 +186,31 @@ def test_cloze():
f.flush() f.flush()
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_chained_mods():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>'
a1 = '<b>sentence</b>'
q2 = '<span style=\"color:red\">en chaine</span>'
a2 = '<i>chained</i>'
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
assert d.addNote(f) == 1
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
def test_modelChange(): def test_modelChange():
deck = getEmptyDeck() deck = getEmptyCol()
basic = deck.models.byName("Basic") basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze") cloze = deck.models.byName("Cloze")
# enable second template and add a note # enable second template and add a note
@ -238,7 +284,7 @@ def test_modelChange():
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1 assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_availOrds(): def test_availOrds():
d = getEmptyDeck() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current(); mm = d.models
t = m['tmpls'][0] t = m['tmpls'][0]
f = d.newNote() f = d.newNote()

View file

@ -1,171 +0,0 @@
# coding: utf-8
import nose, os, time
from tests.shared import assertException
from anki.sync import Syncer, FullSyncer, RemoteServer, \
MediaSyncer, RemoteMediaServer, httpCon
from anki import Collection as aopen
deck1=None
deck2=None
client=None
server=None
server2=None
# Remote tests
##########################################################################
import tests.test_sync as ts
from tests.test_sync import setup_basic
import anki.sync
anki.sync.SYNC_URL = "http://localhost:5000/sync/"
TEST_USER = "synctest@ichi2.net"
TEST_PASS = "abc123"
TEST_HKEY = "WqYF0m7fOHCNPI4a"
TEST_REMOTE = True
def setup_remote():
setup_basic()
# mark deck1 as changed
ts.deck1.save()
ts.server = RemoteServer(TEST_HKEY)
ts.client.server = ts.server
@nose.with_setup(setup_remote)
def test_meta():
global TEST_REMOTE
try:
# if the key is wrong, meta returns nothing
ts.server.hkey = "abc"
assert not ts.server.meta()
except Exception, e:
if e.errno == 61:
TEST_REMOTE = False
print "aborting; server offline"
return
ts.server.hkey = TEST_HKEY
meta = ts.server.meta()
assert meta['mod']
assert meta['scm']
assert meta['mod'] != ts.client.col.mod
assert abs(meta['ts'] - time.time()) < 3
@nose.with_setup(setup_remote)
def test_hkey():
if not TEST_REMOTE:
return
assert not ts.server.hostKey(TEST_USER, "wrongpass")
ts.server.hkey = "willchange"
k = ts.server.hostKey(TEST_USER, TEST_PASS)
assert k == ts.server.hkey == TEST_HKEY
@nose.with_setup(setup_remote)
def test_download():
if not TEST_REMOTE:
return
f = FullSyncer(ts.client.col, "abc", ts.server.con)
assertException(Exception, f.download)
f.hkey = TEST_HKEY
f.download()
@nose.with_setup(setup_remote)
def test_remoteSync():
if not TEST_REMOTE:
return
# not yet associated, so will require a full sync
assert ts.client.sync() == "fullSync"
# upload
f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con)
assert f.upload()
ts.client.col.reopen()
# should report no changes
assert ts.client.sync() == "noChanges"
# bump local col
ts.client.col.setMod()
ts.client.col.save()
assert ts.client.sync() == "success"
# again, no changes
assert ts.client.sync() == "noChanges"
# Remote media tests
##########################################################################
# We can't run useful tests for local media, because the desktop code assumes
# the current directory is the media folder.
def setup_remoteMedia():
setup_basic()
con = httpCon()
ts.server = RemoteMediaServer(TEST_HKEY, con)
ts.server2 = RemoteServer(TEST_HKEY)
ts.client = MediaSyncer(ts.deck1, ts.server)
@nose.with_setup(setup_remoteMedia)
def test_media():
if not TEST_REMOTE:
return
print "media test disabled"
return
ts.server.mediatest("reset")
assert len(os.listdir(ts.deck1.media.dir())) == 0
assert ts.server.mediatest("count") == 0
# initially, nothing to do
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
# add a file
time.sleep(1)
os.chdir(ts.deck1.media.dir())
p = os.path.join(ts.deck1.media.dir(), "foo.jpg")
open(p, "wb").write("foo")
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 0
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
time.sleep(1)
# should have been synced
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1
# if we remove the file, should be removed
os.unlink(p)
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 0
assert ts.server.mediatest("count") == 0
# we should be able to add it again
time.sleep(1)
open(p, "wb").write("foo")
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1
# if we modify it, it should get sent too. also we set the zip size very
# low here, so that we can test splitting into multiple zips
import anki.media; anki.media.SYNC_ZIP_SIZE = 1
time.sleep(1)
open(p, "wb").write("bar")
open(p+"2", "wb").write("baz")
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we lose our media db, we should be able to bring it back in sync
time.sleep(1)
ts.deck1.media.close()
os.unlink(ts.deck1.media.dir()+".db")
ts.deck1.media.connect()
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we send an unchanged file, the server should cope
time.sleep(1)
ts.deck1.media.db.execute("insert into log values ('foo.jpg', 0)")
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we remove foo.jpg on the ts.server, the removal should be synced
assert ts.server.mediatest("removefoo") == "OK"
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1

View file

@ -3,13 +3,13 @@
import time import time
import copy import copy
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
from anki.utils import intTime from anki.utils import intTime
from anki.hooks import addHook from anki.hooks import addHook
def test_clock(): def test_clock():
d = getEmptyDeck() d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60: if (d.sched.dayCutoff - intTime()) < 10*60:
raise Exception("Unit tests will fail around the day rollover.") raise Exception("Unit tests will fail around the day rollover.")
@ -18,12 +18,12 @@ def checkRevIvl(d, c, targetIvl):
return min <= c.ivl <= max return min <= c.ivl <= max
def test_basics(): def test_basics():
d = getEmptyDeck() d = getEmptyCol()
d.reset() d.reset()
assert not d.sched.getCard() assert not d.sched.getCard()
def test_new(): def test_new():
d = getEmptyDeck() d = getEmptyCol()
d.reset() d.reset()
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
@ -43,29 +43,31 @@ def test_new():
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
assert c.due >= t assert c.due >= t
# the default order should ensure siblings are not seen together, and
# should show all cards # disabled for now, as the learn fudging makes this randomly fail
m = d.models.current(); mm = d.models # # the default order should ensure siblings are not seen together, and
t = mm.newTemplate("Reverse") # # should show all cards
t['qfmt'] = "{{Back}}" # m = d.models.current(); mm = d.models
t['afmt'] = "{{Front}}" # t = mm.newTemplate("Reverse")
mm.addTemplate(m, t) # t['qfmt'] = "{{Back}}"
mm.save(m) # t['afmt'] = "{{Front}}"
f = d.newNote() # mm.addTemplate(m, t)
f['Front'] = u"2"; f['Back'] = u"2" # mm.save(m)
d.addNote(f) # f = d.newNote()
f = d.newNote() # f['Front'] = u"2"; f['Back'] = u"2"
f['Front'] = u"3"; f['Back'] = u"3" # d.addNote(f)
d.addNote(f) # f = d.newNote()
d.reset() # f['Front'] = u"3"; f['Back'] = u"3"
qs = ("2", "3", "2", "3") # d.addNote(f)
for n in range(4): # d.reset()
c = d.sched.getCard() # qs = ("2", "3", "2", "3")
assert qs[n] in c.q() # for n in range(4):
d.sched.answerCard(c, 2) # c = d.sched.getCard()
# assert qs[n] in c.q()
# d.sched.answerCard(c, 2)
def test_newLimits(): def test_newLimits():
d = getEmptyDeck() d = getEmptyCol()
# add some notes # add some notes
g2 = d.decks.id("Default::foo") g2 = d.decks.id("Default::foo")
for i in range(30): for i in range(30):
@ -95,7 +97,7 @@ def test_newLimits():
assert d.sched.newCount == 9 assert d.sched.newCount == 9
def test_newBoxes(): def test_newBoxes():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -108,7 +110,7 @@ def test_newBoxes():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_learn(): def test_learn():
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
@ -176,15 +178,13 @@ def test_learn():
c.queue = 1 c.queue = 1
c.odue = 321 c.odue = 321
c.flush() c.flush()
print "----begin"
d.sched.removeLrn() d.sched.removeLrn()
c.load() c.load()
print c.__dict__
assert c.queue == 2 assert c.queue == 2
assert c.due == 321 assert c.due == 321
def test_learn_collapsed(): def test_learn_collapsed():
d = getEmptyDeck() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = u"1" f['Front'] = u"1"
@ -210,7 +210,7 @@ def test_learn_collapsed():
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day(): def test_learn_day():
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -268,7 +268,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
def test_reviews(): def test_reviews():
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
@ -360,7 +360,7 @@ def test_reviews():
assert c.queue == -1 assert c.queue == -1
def test_button_spacing(): def test_button_spacing():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -382,7 +382,7 @@ def test_button_spacing():
def test_overdue_lapse(): def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2 # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return return
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -415,7 +415,7 @@ def test_overdue_lapse():
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
def test_finished(): def test_finished():
d = getEmptyDeck() d = getEmptyCol()
# nothing due # nothing due
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
@ -434,7 +434,7 @@ def test_finished():
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_nextIvl(): def test_nextIvl():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f) d.addNote(f)
@ -490,7 +490,7 @@ def test_nextIvl():
assert d.sched.nextIvlStr(c, 4) == "10.8 months" assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc(): def test_misc():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -504,7 +504,7 @@ def test_misc():
assert d.sched.getCard() assert d.sched.getCard()
def test_suspend(): def test_suspend():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -547,7 +547,7 @@ def test_suspend():
assert c.did == 1 assert c.did == 1
def test_cram(): def test_cram():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -655,7 +655,7 @@ def test_cram():
assert c.did == 1 assert c.did == 1
def test_cram_rem(): def test_cram_rem():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -676,7 +676,7 @@ def test_cram_rem():
def test_cram_resched(): def test_cram_resched():
# add card # add card
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -780,7 +780,7 @@ def test_cram_resched():
# print c.__dict__ # print c.__dict__
def test_ordcycle(): def test_ordcycle():
d = getEmptyDeck() d = getEmptyCol()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current(); mm = d.models m = d.models.current(); mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
@ -804,7 +804,7 @@ def test_ordcycle():
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_counts_idx(): def test_counts_idx():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f) d.addNote(f)
@ -826,7 +826,7 @@ def test_counts_idx():
assert d.sched.counts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -878,7 +878,7 @@ def test_repCounts():
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_timing(): def test_timing():
d = getEmptyDeck() d = getEmptyCol()
# add a few review cards, due today # add a few review cards, due today
for i in range(5): for i in range(5):
f = d.newNote() f = d.newNote()
@ -904,7 +904,7 @@ def test_timing():
assert c.queue == 1 assert c.queue == 1
def test_collapse(): def test_collapse():
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -918,7 +918,7 @@ def test_collapse():
assert not d.sched.getCard() assert not d.sched.getCard()
def test_deckDue(): def test_deckDue():
d = getEmptyDeck() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -968,7 +968,7 @@ def test_deckDue():
d.sched.deckDueTree() d.sched.deckDueTree()
def test_deckTree(): def test_deckTree():
d = getEmptyDeck() d = getEmptyCol()
d.decks.id("new::b::c") d.decks.id("new::b::c")
d.decks.id("new2") d.decks.id("new2")
# new should not appear twice in tree # new should not appear twice in tree
@ -977,7 +977,7 @@ def test_deckTree():
assert "new" not in names assert "new" not in names
def test_deckFlow(): def test_deckFlow():
d = getEmptyDeck() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -1001,7 +1001,7 @@ def test_deckFlow():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_reorder(): def test_reorder():
d = getEmptyDeck() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -1039,7 +1039,7 @@ def test_reorder():
assert f4.cards()[0].due == 2 assert f4.cards()[0].due == 2
def test_forget(): def test_forget():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -1053,7 +1053,7 @@ def test_forget():
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
d.addNote(f) d.addNote(f)
@ -1069,7 +1069,7 @@ def test_resched():
assert c.ivl == +1 assert c.ivl == +1
def test_norelearn(): def test_norelearn():
d = getEmptyDeck() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"
@ -1090,7 +1090,7 @@ def test_norelearn():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
def test_failmult(): def test_failmult():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f) d.addNote(f)

View file

@ -1,10 +1,10 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
def test_stats(): def test_stats():
d = getEmptyDeck() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "foo" f['Front'] = "foo"
d.addNote(f) d.addNote(f)
@ -18,7 +18,7 @@ def test_stats():
assert d.cardStats(c) assert d.cardStats(c)
def test_graphs_empty(): def test_graphs_empty():
d = getEmptyDeck() d = getEmptyCol()
assert d.stats().report() assert d.stats().report()
def test_graphs(): def test_graphs():

View file

@ -5,7 +5,7 @@ import nose, os, shutil, time
from anki import Collection as aopen, Collection from anki import Collection as aopen, Collection
from anki.utils import intTime from anki.utils import intTime
from anki.sync import Syncer, LocalServer from anki.sync import Syncer, LocalServer
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol, getEmptyDeckWith
# Local tests # Local tests
########################################################################## ##########################################################################
@ -18,7 +18,7 @@ server2=None
def setup_basic(): def setup_basic():
global deck1, deck2, client, server global deck1, deck2, client, server
deck1 = getEmptyDeck() deck1 = getEmptyCol()
# add a note to deck 1 # add a note to deck 1
f = deck1.newNote() f = deck1.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"] f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
@ -26,7 +26,7 @@ def setup_basic():
# answer it # answer it
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4) deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
# repeat for deck2 # repeat for deck2
deck2 = getEmptyDeck(server=True) deck2 = getEmptyDeckWith(server=True)
f = deck2.newNote() f = deck2.newNote()
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"] f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
deck2.addNote(f) deck2.addNote(f)
@ -247,7 +247,7 @@ def test_threeway2():
anki.notes.intTime = lambda x=1: intTime(1000) anki.notes.intTime = lambda x=1: intTime(1000)
def setup(): def setup():
# create collection 1 with a single note # create collection 1 with a single note
c1 = getEmptyDeck() c1 = getEmptyCol()
f = c1.newNote() f = c1.newNote()
f['Front'] = u"startingpoint" f['Front'] = u"startingpoint"
nid = f.id nid = f.id
@ -325,7 +325,7 @@ def _test_speed():
deck1.tags.tags[tx] = -1 deck1.tags.tags[tx] = -1
deck1._usn = -1 deck1._usn = -1
deck1.save() deck1.save()
deck2 = getEmptyDeck(server=True) deck2 = getEmptyDeckWith(server=True)
deck1.scm = deck2.scm = 0 deck1.scm = deck2.scm = 0
server = LocalServer(deck2) server = LocalServer(deck2)
client = Syncer(deck1, server) client = Syncer(deck1, server)

View file

@ -1,11 +1,11 @@
# coding: utf-8 # coding: utf-8
import time import time
from tests.shared import getEmptyDeck from tests.shared import getEmptyCol
from anki.consts import * from anki.consts import *
def test_op(): def test_op():
d = getEmptyDeck() d = getEmptyCol()
# should have no undo by default # should have no undo by default
assert not d.undoName() assert not d.undoName()
# let's adjust a study option # let's adjust a study option
@ -36,7 +36,7 @@ def test_op():
assert d.undoName() == "Review" assert d.undoName() == "Review"
def test_review(): def test_review():
d = getEmptyDeck() d = getEmptyCol()
d.conf['counts'] = COUNT_REMAINING d.conf['counts'] = COUNT_REMAINING
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = u"one"

View file

@ -1,6 +1,6 @@
# coding: utf-8 # coding: utf-8
import datetime, shutil import datetime, shutil, tempfile
from anki import Collection from anki import Collection
from anki.consts import * from anki.consts import *
from shared import getUpgradeDeckPath, testDir from shared import getUpgradeDeckPath, testDir
@ -63,8 +63,9 @@ def test_invalid_ords():
assert deck.db.scalar("select count() from cards where ord = 1") == 1 assert deck.db.scalar("select count() from cards where ord = 1") == 1
def test_upgrade2(): def test_upgrade2():
p = "/tmp/alpha-upgrade.anki2" fd, p = tempfile.mkstemp(suffix=".anki2", prefix="alpha-upgrade")
if os.path.exists(p): if os.path.exists(p):
os.close(fd)
os.unlink(p) os.unlink(p)
shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p) shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p)
col = Collection(p) col = Collection(p)

View file

@ -92,16 +92,7 @@ __author__ = "Hubert Pham"
__version__ = "0.2.4" __version__ = "0.2.4"
__docformat__ = "restructuredtext en" __docformat__ = "restructuredtext en"
import sys
# attempt to import PortAudio
try:
import _portaudio as pa import _portaudio as pa
except ImportError:
print "Please build and install the PortAudio Python " +\
"bindings first."
sys.exit(-1)
# Try to use Python 2.4's built in `set' # Try to use Python 2.4's built in `set'
try: try:

View file

@ -29,7 +29,7 @@ do
echo " * "$py echo " * "$py
pyuic4 $i -o $py pyuic4 $i -o $py
# munge the output to use gettext # munge the output to use gettext
perl -pi.bak -e 's/QtGui.QApplication.translate\(".*?", /_(/; s/, None, QtGui.*/))/' $py perl -pi.bak -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' $py
rm $py.bak rm $py.bak
fi fi
done done